1use std::{collections::BTreeMap, ops::RangeBounds, str::FromStr};
2
3use js_int::{Int, UInt};
4use regex::bytes::Regex;
5#[cfg(feature = "unstable-msc3931")]
6use ruma_macros::StringEnum;
7use serde::{Deserialize, Serialize};
8use serde_json::value::Value as JsonValue;
9use wildmatch::WildMatch;
10
11use crate::{power_levels::NotificationPowerLevels, OwnedRoomId, OwnedUserId, UserId};
12#[cfg(feature = "unstable-msc3931")]
13use crate::{PrivOwnedStr, RoomVersionId};
14
15mod flattened_json;
16mod push_condition_serde;
17mod room_member_count_is;
18
19pub use self::{
20 flattened_json::{FlattenedJson, FlattenedJsonValue, ScalarJsonValue},
21 room_member_count_is::{ComparisonOperator, RoomMemberCountIs},
22};
23
24#[cfg(feature = "unstable-msc3931")]
26#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
27#[derive(Clone, PartialEq, Eq, StringEnum)]
28#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
29pub enum RoomVersionFeature {
30 #[cfg(feature = "unstable-msc3932")]
36 #[ruma_enum(rename = "org.matrix.msc3932.extensible_events")]
37 ExtensibleEvents,
38
39 #[doc(hidden)]
40 _Custom(PrivOwnedStr),
41}
42
43#[cfg(feature = "unstable-msc3931")]
44impl RoomVersionFeature {
45 pub fn list_for_room_version(version: &RoomVersionId) -> Vec<Self> {
47 match version {
48 RoomVersionId::V1
49 | RoomVersionId::V2
50 | RoomVersionId::V3
51 | RoomVersionId::V4
52 | RoomVersionId::V5
53 | RoomVersionId::V6
54 | RoomVersionId::V7
55 | RoomVersionId::V8
56 | RoomVersionId::V9
57 | RoomVersionId::V10
58 | RoomVersionId::V11
59 | RoomVersionId::_Custom(_) => vec![],
60 }
61 }
62}
63
64#[derive(Clone, Debug)]
66#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
67pub enum PushCondition {
68 EventMatch {
70 key: String,
74
75 pattern: String,
80 },
81
82 ContainsDisplayName,
85
86 RoomMemberCount {
88 is: RoomMemberCountIs,
90 },
91
92 SenderNotificationPermission {
95 key: String,
100 },
101
102 #[cfg(feature = "unstable-msc3931")]
104 RoomVersionSupports {
105 feature: RoomVersionFeature,
107 },
108
109 EventPropertyIs {
111 key: String,
115
116 value: ScalarJsonValue,
118 },
119
120 EventPropertyContains {
122 key: String,
126
127 value: ScalarJsonValue,
129 },
130
131 #[doc(hidden)]
132 _Custom(_CustomPushCondition),
133}
134
135pub(super) fn check_event_match(
136 event: &FlattenedJson,
137 key: &str,
138 pattern: &str,
139 context: &PushConditionRoomCtx,
140) -> bool {
141 let value = match key {
142 "room_id" => context.room_id.as_str(),
143 _ => match event.get_str(key) {
144 Some(v) => v,
145 None => return false,
146 },
147 };
148
149 value.matches_pattern(pattern, key == "content.body")
150}
151
152impl PushCondition {
153 pub fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
161 if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
162 return false;
163 }
164
165 match self {
166 Self::EventMatch { key, pattern } => check_event_match(event, key, pattern, context),
167 Self::ContainsDisplayName => {
168 let value = match event.get_str("content.body") {
169 Some(v) => v,
170 None => return false,
171 };
172
173 value.matches_pattern(&context.user_display_name, true)
174 }
175 Self::RoomMemberCount { is } => is.contains(&context.member_count),
176 Self::SenderNotificationPermission { key } => {
177 let Some(power_levels) = &context.power_levels else {
178 return false;
179 };
180
181 let sender_id = match event.get_str("sender") {
182 Some(v) => match <&UserId>::try_from(v) {
183 Ok(u) => u,
184 Err(_) => return false,
185 },
186 None => return false,
187 };
188
189 let sender_level =
190 power_levels.users.get(sender_id).unwrap_or(&power_levels.users_default);
191
192 match power_levels.notifications.get(key) {
193 Some(l) => sender_level >= l,
194 None => false,
195 }
196 }
197 #[cfg(feature = "unstable-msc3931")]
198 Self::RoomVersionSupports { feature } => match feature {
199 RoomVersionFeature::ExtensibleEvents => {
200 context.supported_features.contains(&RoomVersionFeature::ExtensibleEvents)
201 }
202 RoomVersionFeature::_Custom(_) => false,
203 },
204 Self::EventPropertyIs { key, value } => event.get(key).is_some_and(|v| v == value),
205 Self::EventPropertyContains { key, value } => event
206 .get(key)
207 .and_then(FlattenedJsonValue::as_array)
208 .is_some_and(|a| a.contains(value)),
209 Self::_Custom(_) => false,
210 }
211 }
212}
213
214#[doc(hidden)]
216#[derive(Clone, Debug, Deserialize, Serialize)]
217#[allow(clippy::exhaustive_structs)]
218pub struct _CustomPushCondition {
219 kind: String,
221
222 #[serde(flatten)]
224 data: BTreeMap<String, JsonValue>,
225}
226
227#[derive(Clone, Debug)]
229#[allow(clippy::exhaustive_structs)]
230pub struct PushConditionRoomCtx {
231 pub room_id: OwnedRoomId,
233
234 pub member_count: UInt,
236
237 pub user_id: OwnedUserId,
239
240 pub user_display_name: String,
242
243 pub power_levels: Option<PushConditionPowerLevelsCtx>,
247
248 #[cfg(feature = "unstable-msc3931")]
250 pub supported_features: Vec<RoomVersionFeature>,
251}
252
253#[derive(Clone, Debug)]
255#[allow(clippy::exhaustive_structs)]
256pub struct PushConditionPowerLevelsCtx {
257 pub users: BTreeMap<OwnedUserId, Int>,
259
260 pub users_default: Int,
262
263 pub notifications: NotificationPowerLevels,
265}
266
267trait CharExt {
269 fn is_word_char(&self) -> bool;
271}
272
273impl CharExt for char {
274 fn is_word_char(&self) -> bool {
275 self.is_ascii_alphanumeric() || *self == '_'
276 }
277}
278
279trait StrExt {
281 fn char_len(&self, index: usize) -> usize;
284
285 fn char_at(&self, index: usize) -> char;
288
289 fn find_prev_char(&self, index: usize) -> Option<char>;
294
295 fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool;
303
304 fn matches_word(&self, pattern: &str) -> bool;
313
314 fn wildcards_to_regex(&self) -> String;
318}
319
320impl StrExt for str {
321 fn char_len(&self, index: usize) -> usize {
322 let mut len = 1;
323 while !self.is_char_boundary(index + len) {
324 len += 1;
325 }
326 len
327 }
328
329 fn char_at(&self, index: usize) -> char {
330 let end = index + self.char_len(index);
331 let char_str = &self[index..end];
332 char::from_str(char_str)
333 .unwrap_or_else(|_| panic!("Could not convert str '{char_str}' to char"))
334 }
335
336 fn find_prev_char(&self, index: usize) -> Option<char> {
337 if index == 0 {
338 return None;
339 }
340
341 let mut pos = index - 1;
342 while !self.is_char_boundary(pos) {
343 pos -= 1;
344 }
345 Some(self.char_at(pos))
346 }
347
348 fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool {
349 let value = &self.to_lowercase();
350 let pattern = &pattern.to_lowercase();
351
352 if match_words {
353 value.matches_word(pattern)
354 } else {
355 WildMatch::new(pattern).matches(value)
356 }
357 }
358
359 fn matches_word(&self, pattern: &str) -> bool {
360 if self == pattern {
361 return true;
362 }
363 if pattern.is_empty() {
364 return false;
365 }
366
367 let has_wildcards = pattern.contains(['?', '*']);
368
369 if has_wildcards {
370 let mut chunks: Vec<String> = vec![];
371 let mut prev_wildcard = false;
372 let mut chunk_start = 0;
373
374 for (i, c) in pattern.char_indices() {
375 if matches!(c, '?' | '*') && !prev_wildcard {
376 if i != 0 {
377 chunks.push(regex::escape(&pattern[chunk_start..i]));
378 chunk_start = i;
379 }
380
381 prev_wildcard = true;
382 } else if prev_wildcard {
383 let chunk = &pattern[chunk_start..i];
384 chunks.push(chunk.wildcards_to_regex());
385
386 chunk_start = i;
387 prev_wildcard = false;
388 }
389 }
390
391 let len = pattern.len();
392 if !prev_wildcard {
393 chunks.push(regex::escape(&pattern[chunk_start..len]));
394 } else if prev_wildcard {
395 let chunk = &pattern[chunk_start..len];
396 chunks.push(chunk.wildcards_to_regex());
397 }
398
399 let regex = format!(r"(?-u:^|\W|\b){}(?-u:\b|\W|$)", chunks.concat());
402 let re = Regex::new(®ex).expect("regex construction should succeed");
403 re.is_match(self.as_bytes())
404 } else {
405 match self.find(pattern) {
406 Some(start) => {
407 let end = start + pattern.len();
408
409 let word_boundary_start = !self.char_at(start).is_word_char()
411 || !self.find_prev_char(start).is_some_and(|c| c.is_word_char());
412
413 if word_boundary_start {
414 let word_boundary_end = end == self.len()
415 || !self.find_prev_char(end).unwrap().is_word_char()
416 || !self.char_at(end).is_word_char();
417
418 if word_boundary_end {
419 return true;
420 }
421 }
422
423 let non_word_str = &self[start..];
425 let non_word = match non_word_str.find(|c: char| !c.is_word_char()) {
426 Some(pos) => pos,
427 None => return false,
428 };
429
430 let word_str = &non_word_str[non_word..];
431 let word = match word_str.find(|c: char| c.is_word_char()) {
432 Some(pos) => pos,
433 None => return false,
434 };
435
436 word_str[word..].matches_word(pattern)
437 }
438 None => false,
439 }
440 }
441 }
442
443 fn wildcards_to_regex(&self) -> String {
444 let question_marks = self.matches('?').count();
448
449 if self.contains('*') {
450 format!(".{{{question_marks},}}")
451 } else {
452 format!(".{{{question_marks}}}")
453 }
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use std::collections::BTreeMap;
460
461 use assert_matches2::assert_matches;
462 use js_int::{int, uint};
463 use serde_json::{
464 from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
465 };
466
467 use super::{
468 FlattenedJson, PushCondition, PushConditionPowerLevelsCtx, PushConditionRoomCtx,
469 RoomMemberCountIs, StrExt,
470 };
471 use crate::{
472 owned_room_id, owned_user_id, power_levels::NotificationPowerLevels, serde::Raw,
473 OwnedUserId,
474 };
475
476 #[test]
477 fn serialize_event_match_condition() {
478 let json_data = json!({
479 "key": "content.msgtype",
480 "kind": "event_match",
481 "pattern": "m.notice"
482 });
483 assert_eq!(
484 to_json_value(PushCondition::EventMatch {
485 key: "content.msgtype".into(),
486 pattern: "m.notice".into(),
487 })
488 .unwrap(),
489 json_data
490 );
491 }
492
493 #[test]
494 fn serialize_contains_display_name_condition() {
495 assert_eq!(
496 to_json_value(PushCondition::ContainsDisplayName).unwrap(),
497 json!({ "kind": "contains_display_name" })
498 );
499 }
500
501 #[test]
502 fn serialize_room_member_count_condition() {
503 let json_data = json!({
504 "is": "2",
505 "kind": "room_member_count"
506 });
507 assert_eq!(
508 to_json_value(PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) })
509 .unwrap(),
510 json_data
511 );
512 }
513
514 #[test]
515 fn serialize_sender_notification_permission_condition() {
516 let json_data = json!({
517 "key": "room",
518 "kind": "sender_notification_permission"
519 });
520 assert_eq!(
521 json_data,
522 to_json_value(PushCondition::SenderNotificationPermission { key: "room".into() })
523 .unwrap()
524 );
525 }
526
527 #[test]
528 fn deserialize_event_match_condition() {
529 let json_data = json!({
530 "key": "content.msgtype",
531 "kind": "event_match",
532 "pattern": "m.notice"
533 });
534 assert_matches!(
535 from_json_value::<PushCondition>(json_data).unwrap(),
536 PushCondition::EventMatch { key, pattern }
537 );
538 assert_eq!(key, "content.msgtype");
539 assert_eq!(pattern, "m.notice");
540 }
541
542 #[test]
543 fn deserialize_contains_display_name_condition() {
544 assert_matches!(
545 from_json_value::<PushCondition>(json!({ "kind": "contains_display_name" })).unwrap(),
546 PushCondition::ContainsDisplayName
547 );
548 }
549
550 #[test]
551 fn deserialize_room_member_count_condition() {
552 let json_data = json!({
553 "is": "2",
554 "kind": "room_member_count"
555 });
556 assert_matches!(
557 from_json_value::<PushCondition>(json_data).unwrap(),
558 PushCondition::RoomMemberCount { is }
559 );
560 assert_eq!(is, RoomMemberCountIs::from(uint!(2)));
561 }
562
563 #[test]
564 fn deserialize_sender_notification_permission_condition() {
565 let json_data = json!({
566 "key": "room",
567 "kind": "sender_notification_permission"
568 });
569 assert_matches!(
570 from_json_value::<PushCondition>(json_data).unwrap(),
571 PushCondition::SenderNotificationPermission { key }
572 );
573 assert_eq!(key, "room");
574 }
575
576 #[test]
577 fn words_match() {
578 assert!("foo bar".matches_word("foo"));
579 assert!(!"Foo bar".matches_word("foo"));
580 assert!(!"foobar".matches_word("foo"));
581 assert!("foobar foo".matches_word("foo"));
582 assert!(!"foobar foobar".matches_word("foo"));
583 assert!(!"foobar bar".matches_word("bar bar"));
584 assert!("foobar bar bar".matches_word("bar bar"));
585 assert!(!"foobar bar barfoo".matches_word("bar bar"));
586 assert!("conduit ⚡️".matches_word("conduit ⚡️"));
587 assert!("conduit ⚡️".matches_word("conduit"));
588 assert!("conduit ⚡️".matches_word("⚡️"));
589 assert!("conduit⚡️".matches_word("conduit"));
590 assert!("conduit⚡️".matches_word("⚡️"));
591 assert!("⚡️conduit".matches_word("conduit"));
592 assert!("⚡️conduit".matches_word("⚡️"));
593 assert!("Ruma Dev👩💻".matches_word("Dev"));
594 assert!("Ruma Dev👩💻".matches_word("👩💻"));
595 assert!("Ruma Dev👩💻".matches_word("Dev👩💻"));
596
597 assert!(!"matrix".matches_word(r"\w*"));
599 assert!(r"\w".matches_word(r"\w*"));
600 assert!(!"matrix".matches_word("[a-z]*"));
601 assert!("[a-z] and [0-9]".matches_word("[a-z]*"));
602 assert!(!"m".matches_word("[[:alpha:]]?"));
603 assert!("[[:alpha:]]!".matches_word("[[:alpha:]]?"));
604
605 assert!("An example event.".matches_word("ex*ple"));
607 assert!("exple".matches_word("ex*ple"));
608 assert!("An exciting triple-whammy".matches_word("ex*ple"));
609 }
610
611 #[test]
612 fn patterns_match() {
613 assert!("foo bar".matches_pattern("foo", true));
615 assert!("Foo bar".matches_pattern("foo", true));
616 assert!(!"foobar".matches_pattern("foo", true));
617 assert!("".matches_pattern("", true));
618 assert!(!"foo".matches_pattern("", true));
619 assert!("foo bar".matches_pattern("foo bar", true));
620 assert!(" foo bar ".matches_pattern("foo bar", true));
621 assert!("baz foo bar baz".matches_pattern("foo bar", true));
622 assert!("foo baré".matches_pattern("foo bar", true));
623 assert!(!"bar foo".matches_pattern("foo bar", true));
624 assert!("foo bar".matches_pattern("foo ", true));
625 assert!("foo ".matches_pattern("foo ", true));
626 assert!("foo ".matches_pattern("foo ", true));
627 assert!(" foo ".matches_pattern("foo ", true));
628
629 assert!("foo bar".matches_pattern("foo*", true));
631 assert!("foo bar".matches_pattern("foo b?r", true));
632 assert!(" foo bar ".matches_pattern("foo b?r", true));
633 assert!("baz foo bar baz".matches_pattern("foo b?r", true));
634 assert!("foo baré".matches_pattern("foo b?r", true));
635 assert!(!"bar foo".matches_pattern("foo b?r", true));
636 assert!("foo bar".matches_pattern("f*o ", true));
637 assert!("foo ".matches_pattern("f*o ", true));
638 assert!("foo ".matches_pattern("f*o ", true));
639 assert!(" foo ".matches_pattern("f*o ", true));
640
641 assert!(!"foo bar".matches_pattern("foo", false));
643 assert!("foo".matches_pattern("foo", false));
644 assert!("foo".matches_pattern("foo*", false));
645 assert!("foobar".matches_pattern("foo*", false));
646 assert!("foo bar".matches_pattern("foo*", false));
647 assert!(!"foo".matches_pattern("foo?", false));
648 assert!("fooo".matches_pattern("foo?", false));
649 assert!("FOO".matches_pattern("foo", false));
650 assert!("".matches_pattern("", false));
651 assert!("".matches_pattern("*", false));
652 assert!(!"foo".matches_pattern("", false));
653
654 assert!("Lunch plans".matches_pattern("lunc?*", false));
656 assert!("LUNCH".matches_pattern("lunc?*", false));
657 assert!(!" lunch".matches_pattern("lunc?*", false));
658 assert!(!"lunc".matches_pattern("lunc?*", false));
659 }
660
661 fn sender() -> OwnedUserId {
662 owned_user_id!("@worthy_whale:server.name")
663 }
664
665 fn push_context() -> PushConditionRoomCtx {
666 let mut users = BTreeMap::new();
667 users.insert(sender(), int!(25));
668
669 let power_levels = PushConditionPowerLevelsCtx {
670 users,
671 users_default: int!(50),
672 notifications: NotificationPowerLevels { room: int!(50) },
673 };
674
675 PushConditionRoomCtx {
676 room_id: owned_room_id!("!room:server.name"),
677 member_count: uint!(3),
678 user_id: owned_user_id!("@gorilla:server.name"),
679 user_display_name: "Groovy Gorilla".into(),
680 power_levels: Some(power_levels),
681 #[cfg(feature = "unstable-msc3931")]
682 supported_features: Default::default(),
683 }
684 }
685
686 fn first_flattened_event() -> FlattenedJson {
687 let raw = serde_json::from_str::<Raw<JsonValue>>(
688 r#"{
689 "sender": "@worthy_whale:server.name",
690 "content": {
691 "msgtype": "m.text",
692 "body": "@room Give a warm welcome to Groovy Gorilla"
693 }
694 }"#,
695 )
696 .unwrap();
697
698 FlattenedJson::from_raw(&raw)
699 }
700
701 fn second_flattened_event() -> FlattenedJson {
702 let raw = serde_json::from_str::<Raw<JsonValue>>(
703 r#"{
704 "sender": "@party_bot:server.name",
705 "content": {
706 "msgtype": "m.notice",
707 "body": "Everybody come to party!"
708 }
709 }"#,
710 )
711 .unwrap();
712
713 FlattenedJson::from_raw(&raw)
714 }
715
716 #[test]
717 fn event_match_applies() {
718 let context = push_context();
719 let first_event = first_flattened_event();
720 let second_event = second_flattened_event();
721
722 let correct_room = PushCondition::EventMatch {
723 key: "room_id".into(),
724 pattern: "!room:server.name".into(),
725 };
726 let incorrect_room = PushCondition::EventMatch {
727 key: "room_id".into(),
728 pattern: "!incorrect:server.name".into(),
729 };
730
731 assert!(correct_room.applies(&first_event, &context));
732 assert!(!incorrect_room.applies(&first_event, &context));
733
734 let keyword =
735 PushCondition::EventMatch { key: "content.body".into(), pattern: "come".into() };
736
737 assert!(!keyword.applies(&first_event, &context));
738 assert!(keyword.applies(&second_event, &context));
739
740 let msgtype =
741 PushCondition::EventMatch { key: "content.msgtype".into(), pattern: "m.notice".into() };
742
743 assert!(!msgtype.applies(&first_event, &context));
744 assert!(msgtype.applies(&second_event, &context));
745 }
746
747 #[test]
748 fn room_member_count_is_applies() {
749 let context = push_context();
750 let event = first_flattened_event();
751
752 let member_count_eq =
753 PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(3)) };
754 let member_count_gt =
755 PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)..) };
756 let member_count_lt =
757 PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(..uint!(3)) };
758
759 assert!(member_count_eq.applies(&event, &context));
760 assert!(member_count_gt.applies(&event, &context));
761 assert!(!member_count_lt.applies(&event, &context));
762 }
763
764 #[test]
765 fn contains_display_name_applies() {
766 let context = push_context();
767 let first_event = first_flattened_event();
768 let second_event = second_flattened_event();
769
770 let contains_display_name = PushCondition::ContainsDisplayName;
771
772 assert!(contains_display_name.applies(&first_event, &context));
773 assert!(!contains_display_name.applies(&second_event, &context));
774 }
775
776 #[test]
777 fn sender_notification_permission_applies() {
778 let context = push_context();
779 let first_event = first_flattened_event();
780 let second_event = second_flattened_event();
781
782 let sender_notification_permission =
783 PushCondition::SenderNotificationPermission { key: "room".into() };
784
785 assert!(!sender_notification_permission.applies(&first_event, &context));
786 assert!(sender_notification_permission.applies(&second_event, &context));
787 }
788
789 #[cfg(feature = "unstable-msc3932")]
790 #[test]
791 fn room_version_supports_applies() {
792 let context_not_matching = push_context();
793
794 let context_matching = PushConditionRoomCtx {
795 room_id: owned_room_id!("!room:server.name"),
796 member_count: uint!(3),
797 user_id: owned_user_id!("@gorilla:server.name"),
798 user_display_name: "Groovy Gorilla".into(),
799 power_levels: context_not_matching.power_levels.clone(),
800 supported_features: vec![super::RoomVersionFeature::ExtensibleEvents],
801 };
802
803 let simple_event_raw = serde_json::from_str::<Raw<JsonValue>>(
804 r#"{
805 "sender": "@worthy_whale:server.name",
806 "content": {
807 "msgtype": "org.matrix.msc3932.extensible_events",
808 "body": "@room Give a warm welcome to Groovy Gorilla"
809 }
810 }"#,
811 )
812 .unwrap();
813 let simple_event = FlattenedJson::from_raw(&simple_event_raw);
814
815 let room_version_condition = PushCondition::RoomVersionSupports {
816 feature: super::RoomVersionFeature::ExtensibleEvents,
817 };
818
819 assert!(room_version_condition.applies(&simple_event, &context_matching));
820 assert!(!room_version_condition.applies(&simple_event, &context_not_matching));
821 }
822
823 #[test]
824 fn event_property_is_applies() {
825 use crate::push::condition::ScalarJsonValue;
826
827 let context = push_context();
828 let event_raw = serde_json::from_str::<Raw<JsonValue>>(
829 r#"{
830 "sender": "@worthy_whale:server.name",
831 "content": {
832 "msgtype": "m.text",
833 "body": "Boom!",
834 "org.fake.boolean": false,
835 "org.fake.number": 13,
836 "org.fake.null": null
837 }
838 }"#,
839 )
840 .unwrap();
841 let event = FlattenedJson::from_raw(&event_raw);
842
843 let string_match = PushCondition::EventPropertyIs {
844 key: "content.body".to_owned(),
845 value: "Boom!".into(),
846 };
847 assert!(string_match.applies(&event, &context));
848
849 let string_no_match =
850 PushCondition::EventPropertyIs { key: "content.body".to_owned(), value: "Boom".into() };
851 assert!(!string_no_match.applies(&event, &context));
852
853 let wrong_type =
854 PushCondition::EventPropertyIs { key: "content.body".to_owned(), value: false.into() };
855 assert!(!wrong_type.applies(&event, &context));
856
857 let bool_match = PushCondition::EventPropertyIs {
858 key: r"content.org\.fake\.boolean".to_owned(),
859 value: false.into(),
860 };
861 assert!(bool_match.applies(&event, &context));
862
863 let bool_no_match = PushCondition::EventPropertyIs {
864 key: r"content.org\.fake\.boolean".to_owned(),
865 value: true.into(),
866 };
867 assert!(!bool_no_match.applies(&event, &context));
868
869 let int_match = PushCondition::EventPropertyIs {
870 key: r"content.org\.fake\.number".to_owned(),
871 value: int!(13).into(),
872 };
873 assert!(int_match.applies(&event, &context));
874
875 let int_no_match = PushCondition::EventPropertyIs {
876 key: r"content.org\.fake\.number".to_owned(),
877 value: int!(130).into(),
878 };
879 assert!(!int_no_match.applies(&event, &context));
880
881 let null_match = PushCondition::EventPropertyIs {
882 key: r"content.org\.fake\.null".to_owned(),
883 value: ScalarJsonValue::Null,
884 };
885 assert!(null_match.applies(&event, &context));
886 }
887
888 #[test]
889 fn event_property_contains_applies() {
890 use crate::push::condition::ScalarJsonValue;
891
892 let context = push_context();
893 let event_raw = serde_json::from_str::<Raw<JsonValue>>(
894 r#"{
895 "sender": "@worthy_whale:server.name",
896 "content": {
897 "org.fake.array": ["Boom!", false, 13, null]
898 }
899 }"#,
900 )
901 .unwrap();
902 let event = FlattenedJson::from_raw(&event_raw);
903
904 let wrong_key =
905 PushCondition::EventPropertyContains { key: "send".to_owned(), value: false.into() };
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: int!(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: int!(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}