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