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(®ex).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// }