Skip to main content

palpo_core/push/condition/
mod.rs

1use std::{collections::BTreeMap, ops::RangeBounds, str::FromStr};
2
3use palpo_macros::StringEnum;
4use regex::bytes::Regex;
5use salvo::oapi::ToSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::value::Value as JsonValue;
8use wildmatch::WildMatch;
9
10use crate::{OwnedRoomId, OwnedUserId, UserId, power_levels::NotificationPowerLevels};
11use crate::{PrivOwnedStr, RoomVersionId};
12
13mod flattened_json;
14mod push_condition_serde;
15mod room_member_count_is;
16
17pub use self::{
18    flattened_json::{FlattenedJson, FlattenedJsonValue, ScalarJsonValue},
19    room_member_count_is::{ComparisonOperator, RoomMemberCountIs},
20};
21
22/// Features supported by room versions.
23
24#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
25#[derive(ToSchema, Clone, PartialEq, Eq, StringEnum)]
26pub enum RoomVersionFeature {
27    /// m.extensible_events
28    ///
29    /// The room supports [extensible events].
30    ///
31    /// [extensible events]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767
32    #[palpo_enum(rename = "org.matrix.msc3932.extensible_events")]
33    ExtensibleEvents,
34
35    #[doc(hidden)]
36    #[salvo(schema(skip))]
37    _Custom(PrivOwnedStr),
38}
39
40impl RoomVersionFeature {
41    /// Get the default features for the given room version.
42    pub fn list_for_room_version(version: &RoomVersionId) -> Vec<Self> {
43        match version {
44            RoomVersionId::V1
45            | RoomVersionId::V2
46            | RoomVersionId::V3
47            | RoomVersionId::V4
48            | RoomVersionId::V5
49            | RoomVersionId::V6
50            | RoomVersionId::V7
51            | RoomVersionId::V8
52            | RoomVersionId::V9
53            | RoomVersionId::V10
54            | RoomVersionId::V11
55            | RoomVersionId::_Custom(_) => vec![],
56        }
57    }
58}
59
60/// A condition that must apply for an associated push rule's action to be taken.
61#[derive(ToSchema, Clone, Debug)]
62pub enum PushCondition {
63    /// A glob pattern match on a field of the event.
64    EventMatch {
65        /// The [dot-separated path] of the property of the event to match.
66        ///
67        /// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
68        key: String,
69
70        /// The glob-style pattern to match against.
71        ///
72        /// Patterns with no special glob characters should be treated as having asterisks
73        /// prepended and appended when testing the condition.
74        pattern: String,
75    },
76
77    /// Matches unencrypted messages where `content.body` contains the owner's display name in that
78    /// room.
79    ContainsDisplayName,
80
81    /// Matches the current number of members in the room.
82    RoomMemberCount {
83        /// The condition on the current number of members in the room.
84        is: RoomMemberCountIs,
85    },
86
87    /// Takes into account the current power levels in the room, ensuring the sender of the event
88    /// has high enough power to trigger the notification.
89    SenderNotificationPermission {
90        /// The field in the power level event the user needs a minimum power level for.
91        ///
92        /// Fields must be specified under the `notifications` property in the power level event's
93        /// `content`.
94        key: String,
95    },
96
97    /// Apply the rule only to rooms that support a given feature.
98    RoomVersionSupports {
99        /// The feature the room must support for the push rule to apply.
100        feature: RoomVersionFeature,
101    },
102
103    /// Exact value match on a property of the event.
104    EventPropertyIs {
105        /// The [dot-separated path] of the property of the event to match.
106        ///
107        /// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
108        key: String,
109
110        /// The value to match against.
111        #[salvo(schema(value_type = Object, additional_properties = true))]
112        value: ScalarJsonValue,
113    },
114
115    /// Exact value match on a value in an array property of the event.
116    EventPropertyContains {
117        /// The [dot-separated path] of the property of the event to match.
118        ///
119        /// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
120        key: String,
121
122        /// The value to match against.
123        #[salvo(schema(value_type = Object, additional_properties = true))]
124        value: ScalarJsonValue,
125    },
126
127    #[doc(hidden)]
128    #[salvo(schema(skip))]
129    _Custom(_CustomPushCondition),
130}
131
132pub(super) fn check_event_match(
133    event: &FlattenedJson,
134    key: &str,
135    pattern: &str,
136    context: &PushConditionRoomCtx,
137) -> bool {
138    let value = match key {
139        "room_id" => context.room_id.as_str(),
140        _ => match event.get_str(key) {
141            Some(v) => v,
142            None => return false,
143        },
144    };
145
146    value.matches_pattern(pattern, key == "content.body")
147}
148
149impl PushCondition {
150    /// Check if this condition applies to the event.
151    ///
152    /// # Arguments
153    ///
154    /// * `event` - The flattened JSON representation of a room message event.
155    /// * `context` - The context of the room at the time of the event. If the power levels context
156    ///   is missing from it, conditions that depend on it will never apply.
157    pub fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
158        if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
159            return false;
160        }
161
162        match self {
163            Self::EventMatch { key, pattern } => check_event_match(event, key, pattern, context),
164            Self::ContainsDisplayName => {
165                let value = match event.get_str("content.body") {
166                    Some(v) => v,
167                    None => return false,
168                };
169
170                value.matches_pattern(&context.user_display_name, true)
171            }
172            Self::RoomMemberCount { is } => is.contains(&context.member_count),
173            Self::SenderNotificationPermission { key } => {
174                let Some(power_levels) = &context.power_levels else {
175                    return false;
176                };
177
178                let sender_id = match event.get_str("sender") {
179                    Some(v) => match <&UserId>::try_from(v) {
180                        Ok(u) => u,
181                        Err(_) => return false,
182                    },
183                    None => return false,
184                };
185
186                let sender_level = power_levels.users.get(sender_id).unwrap_or(&power_levels.users_default);
187
188                match power_levels.notifications.get(key) {
189                    Some(l) => sender_level >= l,
190                    None => false,
191                }
192            }
193
194            Self::RoomVersionSupports { feature } => match feature {
195                RoomVersionFeature::ExtensibleEvents => context
196                    .supported_features
197                    .contains(&RoomVersionFeature::ExtensibleEvents),
198                RoomVersionFeature::_Custom(_) => false,
199            },
200            Self::EventPropertyIs { key, value } => event.get(key).is_some_and(|v| v == value),
201            Self::EventPropertyContains { key, value } => event
202                .get(key)
203                .and_then(FlattenedJsonValue::as_array)
204                .is_some_and(|a| a.contains(value)),
205            Self::_Custom(_) => false,
206        }
207    }
208}
209
210/// An unknown push condition.
211#[doc(hidden)]
212#[derive(Clone, Debug, Deserialize, Serialize)]
213#[allow(clippy::exhaustive_structs)]
214pub struct _CustomPushCondition {
215    /// The kind of the condition.
216    kind: String,
217
218    /// The additional fields that the condition contains.
219    #[serde(flatten)]
220    data: BTreeMap<String, JsonValue>,
221}
222
223/// The context of the room associated to an event to be able to test all push conditions.
224#[derive(Clone, Debug)]
225#[allow(clippy::exhaustive_structs)]
226pub struct PushConditionRoomCtx {
227    /// The ID of the room.
228    pub room_id: OwnedRoomId,
229
230    /// The number of members in the room.
231    pub member_count: u64,
232
233    /// The user's matrix ID.
234    pub user_id: OwnedUserId,
235
236    /// The display name of the current user in the room.
237    pub user_display_name: String,
238
239    /// The room power levels context for the room.
240    ///
241    /// If this is missing, push rules that require this will never match.
242    pub power_levels: Option<PushConditionPowerLevelsCtx>,
243
244    /// The list of features this room's version or the room itself supports.
245    pub supported_features: Vec<RoomVersionFeature>,
246}
247
248/// The room power levels context to be able to test the corresponding push conditions.
249#[derive(Clone, Debug)]
250#[allow(clippy::exhaustive_structs)]
251pub struct PushConditionPowerLevelsCtx {
252    /// The power levels of the users of the room.
253    pub users: BTreeMap<OwnedUserId, i64>,
254
255    /// The default power level of the users of the room.
256    pub users_default: i64,
257
258    /// The notification power levels of the room.
259    pub notifications: NotificationPowerLevels,
260}
261
262/// Additional functions for character matching.
263trait CharExt {
264    /// Whether or not this char can be part of a word.
265    fn is_word_char(&self) -> bool;
266}
267
268impl CharExt for char {
269    fn is_word_char(&self) -> bool {
270        self.is_ascii_alphanumeric() || *self == '_'
271    }
272}
273
274/// Additional functions for string matching.
275trait StrExt {
276    /// Get the length of the char at `index`. The byte index must correspond to
277    /// the start of a char boundary.
278    fn char_len(&self, index: usize) -> usize;
279
280    /// Get the char at `index`. The byte index must correspond to the start of
281    /// a char boundary.
282    fn char_at(&self, index: usize) -> char;
283
284    /// Get the index of the char that is before the char at `index`. The byte index
285    /// must correspond to a char boundary.
286    ///
287    /// Returns `None` if there's no previous char. Otherwise, returns the char.
288    fn find_prev_char(&self, index: usize) -> Option<char>;
289
290    /// Matches this string against `pattern`.
291    ///
292    /// The pattern can be a glob with wildcards `*` and `?`.
293    ///
294    /// The match is case insensitive.
295    ///
296    /// If `match_words` is `true`, checks that the pattern is separated from other words.
297    fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool;
298
299    /// Matches this string against `pattern`, with word boundaries.
300    ///
301    /// The pattern can be a glob with wildcards `*` and `?`.
302    ///
303    /// A word boundary is defined as the start or end of the value, or any character not in the
304    /// sets `[A-Z]`, `[a-z]`, `[0-9]` or `_`.
305    ///
306    /// The match is case sensitive.
307    fn matches_word(&self, pattern: &str) -> bool;
308
309    /// Translate the wildcards in `self` to a regex syntax.
310    ///
311    /// `self` must only contain wildcards.
312    fn wildcards_to_regex(&self) -> String;
313}
314
315impl StrExt for str {
316    fn char_len(&self, index: usize) -> usize {
317        let mut len = 1;
318        while !self.is_char_boundary(index + len) {
319            len += 1;
320        }
321        len
322    }
323
324    fn char_at(&self, index: usize) -> char {
325        let end = index + self.char_len(index);
326        let char_str = &self[index..end];
327        char::from_str(char_str).unwrap_or_else(|_| panic!("Could not convert str '{char_str}' to char"))
328    }
329
330    fn find_prev_char(&self, index: usize) -> Option<char> {
331        if index == 0 {
332            return None;
333        }
334
335        let mut pos = index - 1;
336        while !self.is_char_boundary(pos) {
337            pos -= 1;
338        }
339        Some(self.char_at(pos))
340    }
341
342    fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool {
343        let value = &self.to_lowercase();
344        let pattern = &pattern.to_lowercase();
345
346        if match_words {
347            value.matches_word(pattern)
348        } else {
349            WildMatch::new(pattern).matches(value)
350        }
351    }
352
353    fn matches_word(&self, pattern: &str) -> bool {
354        if self == pattern {
355            return true;
356        }
357        if pattern.is_empty() {
358            return false;
359        }
360
361        let has_wildcards = pattern.contains(|c| matches!(c, '?' | '*'));
362
363        if has_wildcards {
364            let mut chunks: Vec<String> = vec![];
365            let mut prev_wildcard = false;
366            let mut chunk_start = 0;
367
368            for (i, c) in pattern.char_indices() {
369                if matches!(c, '?' | '*') && !prev_wildcard {
370                    if i != 0 {
371                        chunks.push(regex::escape(&pattern[chunk_start..i]));
372                        chunk_start = i;
373                    }
374
375                    prev_wildcard = true;
376                } else if prev_wildcard {
377                    let chunk = &pattern[chunk_start..i];
378                    chunks.push(chunk.wildcards_to_regex());
379
380                    chunk_start = i;
381                    prev_wildcard = false;
382                }
383            }
384
385            let len = pattern.len();
386            if !prev_wildcard {
387                chunks.push(regex::escape(&pattern[chunk_start..len]));
388            } else if prev_wildcard {
389                let chunk = &pattern[chunk_start..len];
390                chunks.push(chunk.wildcards_to_regex());
391            }
392
393            // The word characters in ASCII compatible mode (with the `-u` flag) match the
394            // definition in the spec: any character not in the set `[A-Za-z0-9_]`.
395            let regex = format!(r"(?-u:^|\W|\b){}(?-u:\b|\W|$)", chunks.concat());
396            let re = Regex::new(&regex).expect("regex construction should succeed");
397            re.is_match(self.as_bytes())
398        } else {
399            match self.find(pattern) {
400                Some(start) => {
401                    let end = start + pattern.len();
402
403                    // Look if the match has word boundaries.
404                    let word_boundary_start = !self.char_at(start).is_word_char()
405                        || !self.find_prev_char(start).is_some_and(|c| c.is_word_char());
406
407                    if word_boundary_start {
408                        let word_boundary_end = end == self.len()
409                            || !self.find_prev_char(end).unwrap().is_word_char()
410                            || !self.char_at(end).is_word_char();
411
412                        if word_boundary_end {
413                            return true;
414                        }
415                    }
416
417                    // Find next word.
418                    let non_word_str = &self[start..];
419                    let non_word = match non_word_str.find(|c: char| !c.is_word_char()) {
420                        Some(pos) => pos,
421                        None => return false,
422                    };
423
424                    let word_str = &non_word_str[non_word..];
425                    let word = match word_str.find(|c: char| c.is_word_char()) {
426                        Some(pos) => pos,
427                        None => return false,
428                    };
429
430                    word_str[word..].matches_word(pattern)
431                }
432                None => false,
433            }
434        }
435    }
436
437    fn wildcards_to_regex(&self) -> String {
438        // Simplify pattern to avoid performance issues:
439        // - The glob `?**?**?` is equivalent to the glob `???*`
440        // - The glob `???*` is equivalent to the regex `.{3,}`
441        let question_marks = self.matches('?').count();
442
443        if self.contains('*') {
444            format!(".{{{question_marks},}}")
445        } else {
446            format!(".{{{question_marks}}}")
447        }
448    }
449}
450
451// #[cfg(test)]
452// mod tests {
453//     use std::collections::BTreeMap;
454
455//     use assert_matches2::assert_matches;
456//     use serde_json::{from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue};
457
458//     use super::{
459//         FlattenedJson, PushCondition, PushConditionPowerLevelsCtx, PushConditionRoomCtx, RoomMemberCountIs, StrExt,
460//     };
461//     use crate::{owned_room_id, owned_user_id, power_levels::NotificationPowerLevels, serde::RawJson, OwnedUserId};
462
463//     #[test]
464//     fn serialize_event_match_condition() {
465//         let json_data = json!({
466//             "key": "content.msgtype",
467//             "kind": "event_match",
468//             "pattern": "m.notice"
469//         });
470//         assert_eq!(
471//             to_json_value(PushCondition::EventMatch {
472//                 key: "content.msgtype".into(),
473//                 pattern: "m.notice".into(),
474//             })
475//             .unwrap(),
476//             json_data
477//         );
478//     }
479
480//     #[test]
481//     fn serialize_contains_display_name_condition() {
482//         assert_eq!(
483//             to_json_value(PushCondition::ContainsDisplayName).unwrap(),
484//             json!({ "kind": "contains_display_name" })
485//         );
486//     }
487
488//     #[test]
489//     fn serialize_room_member_count_condition() {
490//         let json_data = json!({
491//             "is": "2",
492//             "kind": "room_member_count"
493//         });
494//         assert_eq!(
495//             to_json_value(PushCondition::RoomMemberCount {
496//                 is: RoomMemberCountIs::from(2)
497//             })
498//             .unwrap(),
499//             json_data
500//         );
501//     }
502
503//     #[test]
504//     fn serialize_sender_notification_permission_condition() {
505//         let json_data = json!({
506//             "key": "room",
507//             "kind": "sender_notification_permission"
508//         });
509//         assert_eq!(
510//             json_data,
511//             to_json_value(PushCondition::SenderNotificationPermission { key: "room".into() }).unwrap()
512//         );
513//     }
514
515//     #[test]
516//     fn deserialize_event_match_condition() {
517//         let json_data = json!({
518//             "key": "content.msgtype",
519//             "kind": "event_match",
520//             "pattern": "m.notice"
521//         });
522//         assert_matches!(
523//             from_json_value::<PushCondition>(json_data).unwrap(),
524//             PushCondition::EventMatch { key, pattern }
525//         );
526//         assert_eq!(key, "content.msgtype");
527//         assert_eq!(pattern, "m.notice");
528//     }
529
530//     #[test]
531//     fn deserialize_contains_display_name_condition() {
532//         assert_matches!(
533//             from_json_value::<PushCondition>(json!({ "kind": "contains_display_name" })).unwrap(),
534//             PushCondition::ContainsDisplayName
535//         );
536//     }
537
538//     #[test]
539//     fn deserialize_room_member_count_condition() {
540//         let json_data = json!({
541//             "is": "2",
542//             "kind": "room_member_count"
543//         });
544//         assert_matches!(
545//             from_json_value::<PushCondition>(json_data).unwrap(),
546//             PushCondition::RoomMemberCount { is }
547//         );
548//         assert_eq!(is, RoomMemberCountIs::from(2));
549//     }
550
551//     #[test]
552//     fn deserialize_sender_notification_permission_condition() {
553//         let json_data = json!({
554//             "key": "room",
555//             "kind": "sender_notification_permission"
556//         });
557//         assert_matches!(
558//             from_json_value::<PushCondition>(json_data).unwrap(),
559//             PushCondition::SenderNotificationPermission { key }
560//         );
561//         assert_eq!(key, "room");
562//     }
563
564//     #[test]
565//     fn words_match() {
566//         assert!("foo bar".matches_word("foo"));
567//         assert!(!"Foo bar".matches_word("foo"));
568//         assert!(!"foobar".matches_word("foo"));
569//         assert!("foobar foo".matches_word("foo"));
570//         assert!(!"foobar foobar".matches_word("foo"));
571//         assert!(!"foobar bar".matches_word("bar bar"));
572//         assert!("foobar bar bar".matches_word("bar bar"));
573//         assert!(!"foobar bar barfoo".matches_word("bar bar"));
574//         assert!("palpo ⚡️".matches_word("palpo ⚡️"));
575//         assert!("palpo ⚡️".matches_word("palpo"));
576//         assert!("palpo ⚡️".matches_word("⚡️"));
577//         assert!("palpo⚡️".matches_word("palpo"));
578//         assert!("palpo⚡️".matches_word("⚡️"));
579//         assert!("⚡️palpo".matches_word("palpo"));
580//         assert!("⚡️palpo".matches_word("⚡️"));
581//         assert!("Palpo Dev👩‍💻".matches_word("Dev"));
582//         assert!("Palpo Dev👩‍💻".matches_word("👩‍💻"));
583//         assert!("Palpo Dev👩‍💻".matches_word("Dev👩‍💻"));
584
585//         // Regex syntax is escaped
586//         assert!(!"matrix".matches_word(r"\w*"));
587//         assert!(r"\w".matches_word(r"\w*"));
588//         assert!(!"matrix".matches_word("[a-z]*"));
589//         assert!("[a-z] and [0-9]".matches_word("[a-z]*"));
590//         assert!(!"m".matches_word("[[:alpha:]]?"));
591//         assert!("[[:alpha:]]!".matches_word("[[:alpha:]]?"));
592
593//         // From the spec: <https://spec.matrix.org/v1.9/client-server-api/#conditions-1>
594//         assert!("An example event.".matches_word("ex*ple"));
595//         assert!("exple".matches_word("ex*ple"));
596//         assert!("An exciting triple-whammy".matches_word("ex*ple"));
597//     }
598
599//     #[test]
600//     fn patterns_match() {
601//         // Word matching without glob
602//         assert!("foo bar".matches_pattern("foo", true));
603//         assert!("Foo bar".matches_pattern("foo", true));
604//         assert!(!"foobar".matches_pattern("foo", true));
605//         assert!("".matches_pattern("", true));
606//         assert!(!"foo".matches_pattern("", true));
607//         assert!("foo bar".matches_pattern("foo bar", true));
608//         assert!(" foo bar ".matches_pattern("foo bar", true));
609//         assert!("baz foo bar baz".matches_pattern("foo bar", true));
610//         assert!("foo baré".matches_pattern("foo bar", true));
611//         assert!(!"bar foo".matches_pattern("foo bar", true));
612//         assert!("foo bar".matches_pattern("foo ", true));
613//         assert!("foo ".matches_pattern("foo ", true));
614//         assert!("foo  ".matches_pattern("foo ", true));
615//         assert!(" foo  ".matches_pattern("foo ", true));
616
617//         // Word matching with glob
618//         assert!("foo bar".matches_pattern("foo*", true));
619//         assert!("foo bar".matches_pattern("foo b?r", true));
620//         assert!(" foo bar ".matches_pattern("foo b?r", true));
621//         assert!("baz foo bar baz".matches_pattern("foo b?r", true));
622//         assert!("foo baré".matches_pattern("foo b?r", true));
623//         assert!(!"bar foo".matches_pattern("foo b?r", true));
624//         assert!("foo bar".matches_pattern("f*o ", true));
625//         assert!("foo ".matches_pattern("f*o ", true));
626//         assert!("foo  ".matches_pattern("f*o ", true));
627//         assert!(" foo  ".matches_pattern("f*o ", true));
628
629//         // Glob matching
630//         assert!(!"foo bar".matches_pattern("foo", false));
631//         assert!("foo".matches_pattern("foo", false));
632//         assert!("foo".matches_pattern("foo*", false));
633//         assert!("foobar".matches_pattern("foo*", false));
634//         assert!("foo bar".matches_pattern("foo*", false));
635//         assert!(!"foo".matches_pattern("foo?", false));
636//         assert!("fooo".matches_pattern("foo?", false));
637//         assert!("FOO".matches_pattern("foo", false));
638//         assert!("".matches_pattern("", false));
639//         assert!("".matches_pattern("*", false));
640//         assert!(!"foo".matches_pattern("", false));
641
642//         // From the spec: <https://spec.matrix.org/v1.9/client-server-api/#conditions-1>
643//         assert!("Lunch plans".matches_pattern("lunc?*", false));
644//         assert!("LUNCH".matches_pattern("lunc?*", false));
645//         assert!(!" lunch".matches_pattern("lunc?*", false));
646//         assert!(!"lunc".matches_pattern("lunc?*", false));
647//     }
648
649//     fn sender() -> OwnedUserId {
650//         owned_user_id!("@worthy_whale:server.name")
651//     }
652
653//     fn push_context() -> PushConditionRoomCtx {
654//         let mut users = BTreeMap::new();
655//         users.insert(sender(), 25);
656
657//         let power_levels = PushConditionPowerLevelsCtx {
658//             users,
659//             users_default: 50,
660//             notifications: NotificationPowerLevels { room: 50 },
661//         };
662
663//         PushConditionRoomCtx {
664//             room_id: owned_room_id!("!room:server.name"),
665//             member_count: u3,
666//             user_id: owned_user_id!("@gorilla:server.name"),
667//             user_display_name: "Groovy Gorilla".into(),
668//             power_levels: Some(power_levels),
669
670//             supported_features: Default::default(),
671//         }
672//     }
673
674//     fn first_flattened_event() -> FlattenedJson {
675//         let raw = serde_json::from_str::<RawJson<JsonValue>>(
676//             r#"{
677//                 "sender": "@worthy_whale:server.name",
678//                 "content": {
679//                     "msgtype": "m.text",
680//                     "body": "@room Give a warm welcome to Groovy Gorilla"
681//                 }
682//             }"#,
683//         )
684//         .unwrap();
685
686//         FlattenedJson::from_raw(&raw)
687//     }
688
689//     fn second_flattened_event() -> FlattenedJson {
690//         let raw = serde_json::from_str::<RawJson<JsonValue>>(
691//             r#"{
692//                 "sender": "@party_bot:server.name",
693//                 "content": {
694//                     "msgtype": "m.notice",
695//                     "body": "Everybody come to party!"
696//                 }
697//             }"#,
698//         )
699//         .unwrap();
700
701//         FlattenedJson::from_raw(&raw)
702//     }
703
704//     #[test]
705//     fn event_match_applies() {
706//         let context = push_context();
707//         let first_event = first_flattened_event();
708//         let second_event = second_flattened_event();
709
710//         let correct_room = PushCondition::EventMatch {
711//             key: "room_id".into(),
712//             pattern: "!room:server.name".into(),
713//         };
714//         let incorrect_room = PushCondition::EventMatch {
715//             key: "room_id".into(),
716//             pattern: "!incorrect:server.name".into(),
717//         };
718
719//         assert!(correct_room.applies(&first_event, &context));
720//         assert!(!incorrect_room.applies(&first_event, &context));
721
722//         let keyword = PushCondition::EventMatch {
723//             key: "content.body".into(),
724//             pattern: "come".into(),
725//         };
726
727//         assert!(!keyword.applies(&first_event, &context));
728//         assert!(keyword.applies(&second_event, &context));
729
730//         let msgtype = PushCondition::EventMatch {
731//             key: "content.msgtype".into(),
732//             pattern: "m.notice".into(),
733//         };
734
735//         assert!(!msgtype.applies(&first_event, &context));
736//         assert!(msgtype.applies(&second_event, &context));
737//     }
738
739//     #[test]
740//     fn room_member_count_is_applies() {
741//         let context = push_context();
742//         let event = first_flattened_event();
743
744//         let member_count_eq = PushCondition::RoomMemberCount {
745//             is: RoomMemberCountIs::from(u3),
746//         };
747//         let member_count_gt = PushCondition::RoomMemberCount {
748//             is: RoomMemberCountIs::from(u2..),
749//         };
750//         let member_count_lt = PushCondition::RoomMemberCount {
751//             is: RoomMemberCountIs::from(..u3),
752//         };
753
754//         assert!(member_count_eq.applies(&event, &context));
755//         assert!(member_count_gt.applies(&event, &context));
756//         assert!(!member_count_lt.applies(&event, &context));
757//     }
758
759//     #[test]
760//     fn contains_display_name_applies() {
761//         let context = push_context();
762//         let first_event = first_flattened_event();
763//         let second_event = second_flattened_event();
764
765//         let contains_display_name = PushCondition::ContainsDisplayName;
766
767//         assert!(contains_display_name.applies(&first_event, &context));
768//         assert!(!contains_display_name.applies(&second_event, &context));
769//     }
770
771//     #[test]
772//     fn sender_notification_permission_applies() {
773//         let context = push_context();
774//         let first_event = first_flattened_event();
775//         let second_event = second_flattened_event();
776
777//         let sender_notification_permission = PushCondition::SenderNotificationPermission { key: "room".into() };
778
779//         assert!(!sender_notification_permission.applies(&first_event, &context));
780//         assert!(sender_notification_permission.applies(&second_event, &context));
781//     }
782
783//     #[cfg(feature = "unstable-msc3932")]
784//     #[test]
785//     fn room_version_supports_applies() {
786//         let context_not_matching = push_context();
787
788//         let context_matching = PushConditionRoomCtx {
789//             room_id: owned_room_id!("!room:server.name"),
790//             member_count: u3,
791//             user_id: owned_user_id!("@gorilla:server.name"),
792//             user_display_name: "Groovy Gorilla".into(),
793//             power_levels: context_not_matching.power_levels.clone(),
794//             supported_features: vec![super::RoomVersionFeature::ExtensibleEvents],
795//         };
796
797//         let simple_event_raw = serde_json::from_str::<RawJson<JsonValue>>(
798//             r#"{
799//                 "sender": "@worthy_whale:server.name",
800//                 "content": {
801//                     "msgtype": "org.matrix.msc3932.extensible_events",
802//                     "body": "@room Give a warm welcome to Groovy Gorilla"
803//                 }
804//             }"#,
805//         )
806//         .unwrap();
807//         let simple_event = FlattenedJson::from_raw(&simple_event_raw);
808
809//         let room_version_condition = PushCondition::RoomVersionSupports {
810//             feature: super::RoomVersionFeature::ExtensibleEvents,
811//         };
812
813//         assert!(room_version_condition.applies(&simple_event, &context_matching));
814//         assert!(!room_version_condition.applies(&simple_event, &context_not_matching));
815//     }
816
817//     #[test]
818//     fn event_property_is_applies() {
819//         use crate::push::condition::ScalarJsonValue;
820
821//         let context = push_context();
822//         let event_raw = serde_json::from_str::<RawJson<JsonValue>>(
823//             r#"{
824//                 "sender": "@worthy_whale:server.name",
825//                 "content": {
826//                     "msgtype": "m.text",
827//                     "body": "Boom!",
828//                     "org.fake.boolean": false,
829//                     "org.fake.number": 13,
830//                     "org.fake.null": null
831//                 }
832//             }"#,
833//         )
834//         .unwrap();
835//         let event = FlattenedJson::from_raw(&event_raw);
836
837//         let string_match = PushCondition::EventPropertyIs {
838//             key: "content.body".to_owned(),
839//             value: "Boom!".into(),
840//         };
841//         assert!(string_match.applies(&event, &context));
842
843//         let string_no_match = PushCondition::EventPropertyIs {
844//             key: "content.body".to_owned(),
845//             value: "Boom".into(),
846//         };
847//         assert!(!string_no_match.applies(&event, &context));
848
849//         let wrong_type = PushCondition::EventPropertyIs {
850//             key: "content.body".to_owned(),
851//             value: false.into(),
852//         };
853//         assert!(!wrong_type.applies(&event, &context));
854
855//         let bool_match = PushCondition::EventPropertyIs {
856//             key: r"content.org\.fake\.boolean".to_owned(),
857//             value: false.into(),
858//         };
859//         assert!(bool_match.applies(&event, &context));
860
861//         let bool_no_match = PushCondition::EventPropertyIs {
862//             key: r"content.org\.fake\.boolean".to_owned(),
863//             value: true.into(),
864//         };
865//         assert!(!bool_no_match.applies(&event, &context));
866
867//         let int_match = PushCondition::EventPropertyIs {
868//             key: r"content.org\.fake\.number".to_owned(),
869//             value: 13.into(),
870//         };
871//         assert!(int_match.applies(&event, &context));
872
873//         let int_no_match = PushCondition::EventPropertyIs {
874//             key: r"content.org\.fake\.number".to_owned(),
875//             value: 130.into(),
876//         };
877//         assert!(!int_no_match.applies(&event, &context));
878
879//         let null_match = PushCondition::EventPropertyIs {
880//             key: r"content.org\.fake\.null".to_owned(),
881//             value: ScalarJsonValue::Null,
882//         };
883//         assert!(null_match.applies(&event, &context));
884//     }
885
886//     #[test]
887//     fn event_property_contains_applies() {
888//         use crate::push::condition::ScalarJsonValue;
889
890//         let context = push_context();
891//         let event_raw = serde_json::from_str::<RawJson<JsonValue>>(
892//             r#"{
893//                 "sender": "@worthy_whale:server.name",
894//                 "content": {
895//                     "org.fake.array": ["Boom!", false, 13, null]
896//                 }
897//             }"#,
898//         )
899//         .unwrap();
900//         let event = FlattenedJson::from_raw(&event_raw);
901
902//         let wrong_key = PushCondition::EventPropertyContains {
903//             key: "send".to_owned(),
904//             value: false.into(),
905//         };
906//         assert!(!wrong_key.applies(&event, &context));
907
908//         let string_match = PushCondition::EventPropertyContains {
909//             key: r"content.org\.fake\.array".to_owned(),
910//             value: "Boom!".into(),
911//         };
912//         assert!(string_match.applies(&event, &context));
913
914//         let string_no_match = PushCondition::EventPropertyContains {
915//             key: r"content.org\.fake\.array".to_owned(),
916//             value: "Boom".into(),
917//         };
918//         assert!(!string_no_match.applies(&event, &context));
919
920//         let bool_match = PushCondition::EventPropertyContains {
921//             key: r"content.org\.fake\.array".to_owned(),
922//             value: false.into(),
923//         };
924//         assert!(bool_match.applies(&event, &context));
925
926//         let bool_no_match = PushCondition::EventPropertyContains {
927//             key: r"content.org\.fake\.array".to_owned(),
928//             value: true.into(),
929//         };
930//         assert!(!bool_no_match.applies(&event, &context));
931
932//         let int_match = PushCondition::EventPropertyContains {
933//             key: r"content.org\.fake\.array".to_owned(),
934//             value: 13.into(),
935//         };
936//         assert!(int_match.applies(&event, &context));
937
938//         let int_no_match = PushCondition::EventPropertyContains {
939//             key: r"content.org\.fake\.array".to_owned(),
940//             value: 130.into(),
941//         };
942//         assert!(!int_no_match.applies(&event, &context));
943
944//         let null_match = PushCondition::EventPropertyContains {
945//             key: r"content.org\.fake\.array".to_owned(),
946//             value: ScalarJsonValue::Null,
947//         };
948//         assert!(null_match.applies(&event, &context));
949//     }
950// }