1use std::hash::{Hash, Hasher};
18
19use indexmap::{Equivalent, IndexSet};
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22use tracing::instrument;
23
24use crate::{
25 serde::{JsonObject, Raw, StringEnum},
26 OwnedRoomId, OwnedUserId, PrivOwnedStr,
27};
28
29mod action;
30mod condition;
31mod iter;
32mod predefined;
33
34#[cfg(feature = "unstable-msc3932")]
35pub use self::condition::RoomVersionFeature;
36pub use self::{
37 action::{Action, Tweak},
38 condition::{
39 ComparisonOperator, FlattenedJson, FlattenedJsonValue, PushCondition,
40 PushConditionPowerLevelsCtx, PushConditionRoomCtx, RoomMemberCountIs, ScalarJsonValue,
41 _CustomPushCondition,
42 },
43 iter::{AnyPushRule, AnyPushRuleRef, RulesetIntoIter, RulesetIter},
44 predefined::{
45 PredefinedContentRuleId, PredefinedOverrideRuleId, PredefinedRuleId,
46 PredefinedUnderrideRuleId,
47 },
48};
49
50#[derive(Clone, Debug, Default, Deserialize, Serialize)]
55#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
56pub struct Ruleset {
57 #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
59 pub content: IndexSet<PatternedPushRule>,
60
61 #[serde(rename = "override", default, skip_serializing_if = "IndexSet::is_empty")]
66 pub override_: IndexSet<ConditionalPushRule>,
67
68 #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
70 pub room: IndexSet<SimplePushRule<OwnedRoomId>>,
71
72 #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
74 pub sender: IndexSet<SimplePushRule<OwnedUserId>>,
75
76 #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
79 pub underride: IndexSet<ConditionalPushRule>,
80}
81
82impl Ruleset {
83 pub fn new() -> Self {
85 Default::default()
86 }
87
88 pub fn iter(&self) -> RulesetIter<'_> {
92 self.into_iter()
93 }
94
95 pub fn insert(
106 &mut self,
107 rule: NewPushRule,
108 after: Option<&str>,
109 before: Option<&str>,
110 ) -> Result<(), InsertPushRuleError> {
111 let rule_id = rule.rule_id();
112 if rule_id.starts_with('.') {
113 return Err(InsertPushRuleError::ServerDefaultRuleId);
114 }
115 if rule_id.contains('/') {
116 return Err(InsertPushRuleError::InvalidRuleId);
117 }
118 if rule_id.contains('\\') {
119 return Err(InsertPushRuleError::InvalidRuleId);
120 }
121 if after.is_some_and(|s| s.starts_with('.')) {
122 return Err(InsertPushRuleError::RelativeToServerDefaultRule);
123 }
124 if before.is_some_and(|s| s.starts_with('.')) {
125 return Err(InsertPushRuleError::RelativeToServerDefaultRule);
126 }
127
128 match rule {
129 NewPushRule::Override(r) => {
130 let mut rule = ConditionalPushRule::from(r);
131
132 if let Some(prev_rule) = self.override_.get(rule.rule_id.as_str()) {
133 rule.enabled = prev_rule.enabled;
134 }
135
136 let default_position = 1;
139
140 insert_and_move_rule(&mut self.override_, rule, default_position, after, before)
141 }
142 NewPushRule::Underride(r) => {
143 let mut rule = ConditionalPushRule::from(r);
144
145 if let Some(prev_rule) = self.underride.get(rule.rule_id.as_str()) {
146 rule.enabled = prev_rule.enabled;
147 }
148
149 insert_and_move_rule(&mut self.underride, rule, 0, after, before)
150 }
151 NewPushRule::Content(r) => {
152 let mut rule = PatternedPushRule::from(r);
153
154 if let Some(prev_rule) = self.content.get(rule.rule_id.as_str()) {
155 rule.enabled = prev_rule.enabled;
156 }
157
158 insert_and_move_rule(&mut self.content, rule, 0, after, before)
159 }
160 NewPushRule::Room(r) => {
161 let mut rule = SimplePushRule::from(r);
162
163 if let Some(prev_rule) = self.room.get(rule.rule_id.as_str()) {
164 rule.enabled = prev_rule.enabled;
165 }
166
167 insert_and_move_rule(&mut self.room, rule, 0, after, before)
168 }
169 NewPushRule::Sender(r) => {
170 let mut rule = SimplePushRule::from(r);
171
172 if let Some(prev_rule) = self.sender.get(rule.rule_id.as_str()) {
173 rule.enabled = prev_rule.enabled;
174 }
175
176 insert_and_move_rule(&mut self.sender, rule, 0, after, before)
177 }
178 }
179 }
180
181 pub fn get(&self, kind: RuleKind, rule_id: impl AsRef<str>) -> Option<AnyPushRuleRef<'_>> {
183 let rule_id = rule_id.as_ref();
184
185 match kind {
186 RuleKind::Override => self.override_.get(rule_id).map(AnyPushRuleRef::Override),
187 RuleKind::Underride => self.underride.get(rule_id).map(AnyPushRuleRef::Underride),
188 RuleKind::Sender => self.sender.get(rule_id).map(AnyPushRuleRef::Sender),
189 RuleKind::Room => self.room.get(rule_id).map(AnyPushRuleRef::Room),
190 RuleKind::Content => self.content.get(rule_id).map(AnyPushRuleRef::Content),
191 RuleKind::_Custom(_) => None,
192 }
193 }
194
195 pub fn set_enabled(
200 &mut self,
201 kind: RuleKind,
202 rule_id: impl AsRef<str>,
203 enabled: bool,
204 ) -> Result<(), RuleNotFoundError> {
205 let rule_id = rule_id.as_ref();
206
207 match kind {
208 RuleKind::Override => {
209 let mut rule = self.override_.get(rule_id).ok_or(RuleNotFoundError)?.clone();
210 rule.enabled = enabled;
211 self.override_.replace(rule);
212 }
213 RuleKind::Underride => {
214 let mut rule = self.underride.get(rule_id).ok_or(RuleNotFoundError)?.clone();
215 rule.enabled = enabled;
216 self.underride.replace(rule);
217 }
218 RuleKind::Sender => {
219 let mut rule = self.sender.get(rule_id).ok_or(RuleNotFoundError)?.clone();
220 rule.enabled = enabled;
221 self.sender.replace(rule);
222 }
223 RuleKind::Room => {
224 let mut rule = self.room.get(rule_id).ok_or(RuleNotFoundError)?.clone();
225 rule.enabled = enabled;
226 self.room.replace(rule);
227 }
228 RuleKind::Content => {
229 let mut rule = self.content.get(rule_id).ok_or(RuleNotFoundError)?.clone();
230 rule.enabled = enabled;
231 self.content.replace(rule);
232 }
233 RuleKind::_Custom(_) => return Err(RuleNotFoundError),
234 }
235
236 Ok(())
237 }
238
239 pub fn set_actions(
244 &mut self,
245 kind: RuleKind,
246 rule_id: impl AsRef<str>,
247 actions: Vec<Action>,
248 ) -> Result<(), RuleNotFoundError> {
249 let rule_id = rule_id.as_ref();
250
251 match kind {
252 RuleKind::Override => {
253 let mut rule = self.override_.get(rule_id).ok_or(RuleNotFoundError)?.clone();
254 rule.actions = actions;
255 self.override_.replace(rule);
256 }
257 RuleKind::Underride => {
258 let mut rule = self.underride.get(rule_id).ok_or(RuleNotFoundError)?.clone();
259 rule.actions = actions;
260 self.underride.replace(rule);
261 }
262 RuleKind::Sender => {
263 let mut rule = self.sender.get(rule_id).ok_or(RuleNotFoundError)?.clone();
264 rule.actions = actions;
265 self.sender.replace(rule);
266 }
267 RuleKind::Room => {
268 let mut rule = self.room.get(rule_id).ok_or(RuleNotFoundError)?.clone();
269 rule.actions = actions;
270 self.room.replace(rule);
271 }
272 RuleKind::Content => {
273 let mut rule = self.content.get(rule_id).ok_or(RuleNotFoundError)?.clone();
274 rule.actions = actions;
275 self.content.replace(rule);
276 }
277 RuleKind::_Custom(_) => return Err(RuleNotFoundError),
278 }
279
280 Ok(())
281 }
282
283 #[instrument(skip_all, fields(context.room_id = %context.room_id))]
290 pub fn get_match<T>(
291 &self,
292 event: &Raw<T>,
293 context: &PushConditionRoomCtx,
294 ) -> Option<AnyPushRuleRef<'_>> {
295 let event = FlattenedJson::from_raw(event);
296
297 if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
298 None
300 } else {
301 self.iter().find(|rule| rule.applies(&event, context))
302 }
303 }
304
305 #[instrument(skip_all, fields(context.room_id = %context.room_id))]
314 pub fn get_actions<T>(&self, event: &Raw<T>, context: &PushConditionRoomCtx) -> &[Action] {
315 self.get_match(event, context).map(|rule| rule.actions()).unwrap_or(&[])
316 }
317
318 pub fn remove(
322 &mut self,
323 kind: RuleKind,
324 rule_id: impl AsRef<str>,
325 ) -> Result<(), RemovePushRuleError> {
326 let rule_id = rule_id.as_ref();
327
328 if let Some(rule) = self.get(kind.clone(), rule_id) {
329 if rule.is_server_default() {
330 return Err(RemovePushRuleError::ServerDefault);
331 }
332 } else {
333 return Err(RemovePushRuleError::NotFound);
334 }
335
336 match kind {
337 RuleKind::Override => {
338 self.override_.shift_remove(rule_id);
339 }
340 RuleKind::Underride => {
341 self.underride.shift_remove(rule_id);
342 }
343 RuleKind::Sender => {
344 self.sender.shift_remove(rule_id);
345 }
346 RuleKind::Room => {
347 self.room.shift_remove(rule_id);
348 }
349 RuleKind::Content => {
350 self.content.shift_remove(rule_id);
351 }
352 RuleKind::_Custom(_) => unreachable!(),
354 }
355
356 Ok(())
357 }
358}
359
360#[derive(Clone, Debug, Deserialize, Serialize)]
369#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
370pub struct SimplePushRule<T> {
371 pub actions: Vec<Action>,
373
374 pub default: bool,
376
377 pub enabled: bool,
379
380 pub rule_id: T,
384}
385
386#[derive(Debug)]
391#[allow(clippy::exhaustive_structs)]
392pub struct SimplePushRuleInit<T> {
393 pub actions: Vec<Action>,
395
396 pub default: bool,
398
399 pub enabled: bool,
401
402 pub rule_id: T,
406}
407
408impl<T> From<SimplePushRuleInit<T>> for SimplePushRule<T> {
409 fn from(init: SimplePushRuleInit<T>) -> Self {
410 let SimplePushRuleInit { actions, default, enabled, rule_id } = init;
411 Self { actions, default, enabled, rule_id }
412 }
413}
414
415impl<T> Hash for SimplePushRule<T>
419where
420 T: Hash,
421{
422 fn hash<H: Hasher>(&self, state: &mut H) {
423 self.rule_id.hash(state);
424 }
425}
426
427impl<T> PartialEq for SimplePushRule<T>
428where
429 T: PartialEq<T>,
430{
431 fn eq(&self, other: &Self) -> bool {
432 self.rule_id == other.rule_id
433 }
434}
435
436impl<T> Eq for SimplePushRule<T> where T: Eq {}
437
438impl<T> Equivalent<SimplePushRule<T>> for str
439where
440 T: AsRef<str>,
441{
442 fn equivalent(&self, key: &SimplePushRule<T>) -> bool {
443 self == key.rule_id.as_ref()
444 }
445}
446
447#[derive(Clone, Debug, Deserialize, Serialize)]
454#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
455pub struct ConditionalPushRule {
456 pub actions: Vec<Action>,
458
459 pub default: bool,
461
462 pub enabled: bool,
464
465 pub rule_id: String,
467
468 #[serde(default)]
473 pub conditions: Vec<PushCondition>,
474}
475
476impl ConditionalPushRule {
477 pub fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
484 if !self.enabled {
485 return false;
486 }
487
488 #[cfg(feature = "unstable-msc3932")]
489 {
490 #[allow(deprecated)]
492 if self.rule_id != PredefinedOverrideRuleId::Master.as_ref()
493 && self.rule_id != PredefinedOverrideRuleId::RoomNotif.as_ref()
494 && self.rule_id != PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
495 {
496 let room_supports_ext_ev =
500 context.supported_features.contains(&RoomVersionFeature::ExtensibleEvents);
501 let rule_has_room_version_supports = self.conditions.iter().any(|condition| {
502 matches!(condition, PushCondition::RoomVersionSupports { .. })
503 });
504
505 if room_supports_ext_ev && !rule_has_room_version_supports {
506 return false;
507 }
508 }
509 }
510
511 #[allow(deprecated)]
513 if (self.rule_id == PredefinedOverrideRuleId::RoomNotif.as_ref()
514 || self.rule_id == PredefinedOverrideRuleId::ContainsDisplayName.as_ref())
515 && event.contains_mentions()
516 {
517 return false;
518 }
519
520 self.conditions.iter().all(|cond| cond.applies(event, context))
521 }
522}
523
524#[derive(Debug)]
529#[allow(clippy::exhaustive_structs)]
530pub struct ConditionalPushRuleInit {
531 pub actions: Vec<Action>,
533
534 pub default: bool,
536
537 pub enabled: bool,
539
540 pub rule_id: String,
542
543 pub conditions: Vec<PushCondition>,
548}
549
550impl From<ConditionalPushRuleInit> for ConditionalPushRule {
551 fn from(init: ConditionalPushRuleInit) -> Self {
552 let ConditionalPushRuleInit { actions, default, enabled, rule_id, conditions } = init;
553 Self { actions, default, enabled, rule_id, conditions }
554 }
555}
556
557impl Hash for ConditionalPushRule {
561 fn hash<H: Hasher>(&self, state: &mut H) {
562 self.rule_id.hash(state);
563 }
564}
565
566impl PartialEq for ConditionalPushRule {
567 fn eq(&self, other: &Self) -> bool {
568 self.rule_id == other.rule_id
569 }
570}
571
572impl Eq for ConditionalPushRule {}
573
574impl Equivalent<ConditionalPushRule> for str {
575 fn equivalent(&self, key: &ConditionalPushRule) -> bool {
576 self == key.rule_id
577 }
578}
579
580#[derive(Clone, Debug, Deserialize, Serialize)]
587#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
588pub struct PatternedPushRule {
589 pub actions: Vec<Action>,
591
592 pub default: bool,
594
595 pub enabled: bool,
597
598 pub rule_id: String,
600
601 pub pattern: String,
603}
604
605impl PatternedPushRule {
606 pub fn applies_to(
613 &self,
614 key: &str,
615 event: &FlattenedJson,
616 context: &PushConditionRoomCtx,
617 ) -> bool {
618 #[allow(deprecated)]
620 if self.rule_id == PredefinedContentRuleId::ContainsUserName.as_ref()
621 && event.contains_mentions()
622 {
623 return false;
624 }
625
626 if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
627 return false;
628 }
629
630 self.enabled && condition::check_event_match(event, key, &self.pattern, context)
631 }
632}
633
634#[derive(Debug)]
639#[allow(clippy::exhaustive_structs)]
640pub struct PatternedPushRuleInit {
641 pub actions: Vec<Action>,
643
644 pub default: bool,
646
647 pub enabled: bool,
649
650 pub rule_id: String,
652
653 pub pattern: String,
655}
656
657impl From<PatternedPushRuleInit> for PatternedPushRule {
658 fn from(init: PatternedPushRuleInit) -> Self {
659 let PatternedPushRuleInit { actions, default, enabled, rule_id, pattern } = init;
660 Self { actions, default, enabled, rule_id, pattern }
661 }
662}
663
664impl Hash for PatternedPushRule {
668 fn hash<H: Hasher>(&self, state: &mut H) {
669 self.rule_id.hash(state);
670 }
671}
672
673impl PartialEq for PatternedPushRule {
674 fn eq(&self, other: &Self) -> bool {
675 self.rule_id == other.rule_id
676 }
677}
678
679impl Eq for PatternedPushRule {}
680
681impl Equivalent<PatternedPushRule> for str {
682 fn equivalent(&self, key: &PatternedPushRule) -> bool {
683 self == key.rule_id
684 }
685}
686
687#[derive(Clone, Debug, Serialize, Deserialize)]
689#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
690pub struct HttpPusherData {
691 pub url: String,
695
696 #[serde(skip_serializing_if = "Option::is_none")]
698 pub format: Option<PushFormat>,
699
700 #[serde(flatten, default, skip_serializing_if = "JsonObject::is_empty")]
702 pub data: JsonObject,
703}
704
705impl HttpPusherData {
706 pub fn new(url: String) -> Self {
708 Self { url, format: None, data: JsonObject::default() }
709 }
710}
711
712#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
717#[derive(Clone, PartialEq, Eq, StringEnum)]
718#[ruma_enum(rename_all = "snake_case")]
719#[non_exhaustive]
720pub enum PushFormat {
721 EventIdOnly,
723
724 #[doc(hidden)]
725 _Custom(PrivOwnedStr),
726}
727
728#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
730#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
731#[ruma_enum(rename_all = "snake_case")]
732#[non_exhaustive]
733pub enum RuleKind {
734 Override,
736
737 Underride,
739
740 Sender,
742
743 Room,
745
746 Content,
748
749 #[doc(hidden)]
750 _Custom(PrivOwnedStr),
751}
752
753#[derive(Clone, Debug)]
755#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
756pub enum NewPushRule {
757 Override(NewConditionalPushRule),
759
760 Content(NewPatternedPushRule),
762
763 Room(NewSimplePushRule<OwnedRoomId>),
765
766 Sender(NewSimplePushRule<OwnedUserId>),
768
769 Underride(NewConditionalPushRule),
771}
772
773impl NewPushRule {
774 pub fn kind(&self) -> RuleKind {
776 match self {
777 NewPushRule::Override(_) => RuleKind::Override,
778 NewPushRule::Content(_) => RuleKind::Content,
779 NewPushRule::Room(_) => RuleKind::Room,
780 NewPushRule::Sender(_) => RuleKind::Sender,
781 NewPushRule::Underride(_) => RuleKind::Underride,
782 }
783 }
784
785 pub fn rule_id(&self) -> &str {
787 match self {
788 NewPushRule::Override(r) => &r.rule_id,
789 NewPushRule::Content(r) => &r.rule_id,
790 NewPushRule::Room(r) => r.rule_id.as_ref(),
791 NewPushRule::Sender(r) => r.rule_id.as_ref(),
792 NewPushRule::Underride(r) => &r.rule_id,
793 }
794 }
795}
796
797#[derive(Clone, Debug, Deserialize, Serialize)]
799#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
800pub struct NewSimplePushRule<T> {
801 pub rule_id: T,
805
806 pub actions: Vec<Action>,
809}
810
811impl<T> NewSimplePushRule<T> {
812 pub fn new(rule_id: T, actions: Vec<Action>) -> Self {
814 Self { rule_id, actions }
815 }
816}
817
818impl<T> From<NewSimplePushRule<T>> for SimplePushRule<T> {
819 fn from(new_rule: NewSimplePushRule<T>) -> Self {
820 let NewSimplePushRule { rule_id, actions } = new_rule;
821 Self { actions, default: false, enabled: true, rule_id }
822 }
823}
824
825#[derive(Clone, Debug, Deserialize, Serialize)]
827#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
828pub struct NewPatternedPushRule {
829 pub rule_id: String,
831
832 pub pattern: String,
834
835 pub actions: Vec<Action>,
838}
839
840impl NewPatternedPushRule {
841 pub fn new(rule_id: String, pattern: String, actions: Vec<Action>) -> Self {
843 Self { rule_id, pattern, actions }
844 }
845}
846
847impl From<NewPatternedPushRule> for PatternedPushRule {
848 fn from(new_rule: NewPatternedPushRule) -> Self {
849 let NewPatternedPushRule { rule_id, pattern, actions } = new_rule;
850 Self { actions, default: false, enabled: true, rule_id, pattern }
851 }
852}
853
854#[derive(Clone, Debug, Deserialize, Serialize)]
856#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
857pub struct NewConditionalPushRule {
858 pub rule_id: String,
860
861 #[serde(default)]
866 pub conditions: Vec<PushCondition>,
867
868 pub actions: Vec<Action>,
871}
872
873impl NewConditionalPushRule {
874 pub fn new(rule_id: String, conditions: Vec<PushCondition>, actions: Vec<Action>) -> Self {
876 Self { rule_id, conditions, actions }
877 }
878}
879
880impl From<NewConditionalPushRule> for ConditionalPushRule {
881 fn from(new_rule: NewConditionalPushRule) -> Self {
882 let NewConditionalPushRule { rule_id, conditions, actions } = new_rule;
883 Self { actions, default: false, enabled: true, rule_id, conditions }
884 }
885}
886
887#[derive(Debug, Error)]
889#[non_exhaustive]
890pub enum InsertPushRuleError {
891 #[error("rule IDs starting with a dot are reserved for server-default rules")]
893 ServerDefaultRuleId,
894
895 #[error("invalid rule ID")]
897 InvalidRuleId,
898
899 #[error("can't place rule relative to server-default rule")]
901 RelativeToServerDefaultRule,
902
903 #[error("The before or after rule could not be found")]
905 UnknownRuleId,
906
907 #[error("before has a higher priority than after")]
909 BeforeHigherThanAfter,
910}
911
912#[derive(Debug, Error)]
914#[non_exhaustive]
915#[error("The rule could not be found")]
916pub struct RuleNotFoundError;
917
918pub fn insert_and_move_rule<T>(
920 set: &mut IndexSet<T>,
921 rule: T,
922 default_position: usize,
923 after: Option<&str>,
924 before: Option<&str>,
925) -> Result<(), InsertPushRuleError>
926where
927 T: Hash + Eq,
928 str: Equivalent<T>,
929{
930 let (from, replaced) = set.replace_full(rule);
931
932 let mut to = default_position;
933
934 if let Some(rule_id) = after {
935 let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
936 to = idx + 1;
937 }
938 if let Some(rule_id) = before {
939 let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
940
941 if idx < to {
942 return Err(InsertPushRuleError::BeforeHigherThanAfter);
943 }
944
945 to = idx;
946 }
947
948 if replaced.is_none() || after.is_some() || before.is_some() {
950 set.move_index(from, to);
951 }
952
953 Ok(())
954}
955
956#[derive(Debug, Error)]
958#[non_exhaustive]
959pub enum RemovePushRuleError {
960 #[error("server-default rules cannot be removed")]
962 ServerDefault,
963
964 #[error("rule not found")]
966 NotFound,
967}
968
969#[cfg(test)]
970mod tests {
971 use std::collections::BTreeMap;
972
973 use assert_matches2::assert_matches;
974 use js_int::{int, uint};
975 use serde_json::{
976 from_value as from_json_value, json, to_value as to_json_value,
977 value::RawValue as RawJsonValue, Value as JsonValue,
978 };
979
980 use super::{
981 action::{Action, Tweak},
982 condition::{
983 PushCondition, PushConditionPowerLevelsCtx, PushConditionRoomCtx, RoomMemberCountIs,
984 },
985 AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule,
986 };
987 use crate::{
988 owned_room_id, owned_user_id,
989 power_levels::NotificationPowerLevels,
990 push::{PredefinedContentRuleId, PredefinedOverrideRuleId},
991 serde::Raw,
992 user_id,
993 };
994
995 fn example_ruleset() -> Ruleset {
996 let mut set = Ruleset::new();
997
998 set.override_.insert(ConditionalPushRule {
999 conditions: vec![PushCondition::EventMatch {
1000 key: "type".into(),
1001 pattern: "m.call.invite".into(),
1002 }],
1003 actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1004 rule_id: ".m.rule.call".into(),
1005 enabled: true,
1006 default: true,
1007 });
1008
1009 set
1010 }
1011
1012 fn power_levels() -> PushConditionPowerLevelsCtx {
1013 PushConditionPowerLevelsCtx {
1014 users: BTreeMap::new(),
1015 users_default: int!(50),
1016 notifications: NotificationPowerLevels { room: int!(50) },
1017 }
1018 }
1019
1020 #[test]
1021 fn iter() {
1022 let mut set = example_ruleset();
1023
1024 let added = set.override_.insert(ConditionalPushRule {
1025 conditions: vec![PushCondition::EventMatch {
1026 key: "room_id".into(),
1027 pattern: "!roomid:matrix.org".into(),
1028 }],
1029 actions: vec![],
1030 rule_id: "!roomid:matrix.org".into(),
1031 enabled: true,
1032 default: false,
1033 });
1034 assert!(added);
1035
1036 let added = set.override_.insert(ConditionalPushRule {
1037 conditions: vec![],
1038 actions: vec![],
1039 rule_id: ".m.rule.suppress_notices".into(),
1040 enabled: false,
1041 default: true,
1042 });
1043 assert!(added);
1044
1045 let mut iter = set.into_iter();
1046
1047 let rule_opt = iter.next();
1048 assert!(rule_opt.is_some());
1049 assert_matches!(
1050 rule_opt.unwrap(),
1051 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1052 );
1053 assert_eq!(rule_id, ".m.rule.call");
1054
1055 let rule_opt = iter.next();
1056 assert!(rule_opt.is_some());
1057 assert_matches!(
1058 rule_opt.unwrap(),
1059 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1060 );
1061 assert_eq!(rule_id, "!roomid:matrix.org");
1062
1063 let rule_opt = iter.next();
1064 assert!(rule_opt.is_some());
1065 assert_matches!(
1066 rule_opt.unwrap(),
1067 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1068 );
1069 assert_eq!(rule_id, ".m.rule.suppress_notices");
1070
1071 assert_matches!(iter.next(), None);
1072 }
1073
1074 #[test]
1075 fn serialize_conditional_push_rule() {
1076 let rule = ConditionalPushRule {
1077 actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1078 default: true,
1079 enabled: true,
1080 rule_id: ".m.rule.call".into(),
1081 conditions: vec![
1082 PushCondition::EventMatch { key: "type".into(), pattern: "m.call.invite".into() },
1083 PushCondition::ContainsDisplayName,
1084 PushCondition::RoomMemberCount { is: RoomMemberCountIs::gt(uint!(2)) },
1085 PushCondition::SenderNotificationPermission { key: "room".into() },
1086 ],
1087 };
1088
1089 let rule_value: JsonValue = to_json_value(rule).unwrap();
1090 assert_eq!(
1091 rule_value,
1092 json!({
1093 "conditions": [
1094 {
1095 "kind": "event_match",
1096 "key": "type",
1097 "pattern": "m.call.invite"
1098 },
1099 {
1100 "kind": "contains_display_name"
1101 },
1102 {
1103 "kind": "room_member_count",
1104 "is": ">2"
1105 },
1106 {
1107 "kind": "sender_notification_permission",
1108 "key": "room"
1109 }
1110 ],
1111 "actions": [
1112 "notify",
1113 {
1114 "set_tweak": "highlight"
1115 }
1116 ],
1117 "rule_id": ".m.rule.call",
1118 "default": true,
1119 "enabled": true
1120 })
1121 );
1122 }
1123
1124 #[test]
1125 fn serialize_simple_push_rule() {
1126 let rule = SimplePushRule {
1127 actions: vec![Action::Notify],
1128 default: false,
1129 enabled: false,
1130 rule_id: owned_room_id!("!roomid:server.name"),
1131 };
1132
1133 let rule_value: JsonValue = to_json_value(rule).unwrap();
1134 assert_eq!(
1135 rule_value,
1136 json!({
1137 "actions": [
1138 "notify"
1139 ],
1140 "rule_id": "!roomid:server.name",
1141 "default": false,
1142 "enabled": false
1143 })
1144 );
1145 }
1146
1147 #[test]
1148 fn serialize_patterned_push_rule() {
1149 let rule = PatternedPushRule {
1150 actions: vec![
1151 Action::Notify,
1152 Action::SetTweak(Tweak::Sound("default".into())),
1153 Action::SetTweak(Tweak::Custom {
1154 name: "dance".into(),
1155 value: RawJsonValue::from_string("true".into()).unwrap(),
1156 }),
1157 ],
1158 default: true,
1159 enabled: true,
1160 pattern: "user_id".into(),
1161 rule_id: ".m.rule.contains_user_name".into(),
1162 };
1163
1164 let rule_value: JsonValue = to_json_value(rule).unwrap();
1165 assert_eq!(
1166 rule_value,
1167 json!({
1168 "actions": [
1169 "notify",
1170 {
1171 "set_tweak": "sound",
1172 "value": "default"
1173 },
1174 {
1175 "set_tweak": "dance",
1176 "value": true
1177 }
1178 ],
1179 "pattern": "user_id",
1180 "rule_id": ".m.rule.contains_user_name",
1181 "default": true,
1182 "enabled": true
1183 })
1184 );
1185 }
1186
1187 #[test]
1188 fn serialize_ruleset() {
1189 let mut set = example_ruleset();
1190
1191 set.override_.insert(ConditionalPushRule {
1192 conditions: vec![
1193 PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
1194 PushCondition::EventMatch { key: "type".into(), pattern: "m.room.message".into() },
1195 ],
1196 actions: vec![
1197 Action::Notify,
1198 Action::SetTweak(Tweak::Sound("default".into())),
1199 Action::SetTweak(Tweak::Highlight(false)),
1200 ],
1201 rule_id: ".m.rule.room_one_to_one".into(),
1202 enabled: true,
1203 default: true,
1204 });
1205 set.content.insert(PatternedPushRule {
1206 actions: vec![
1207 Action::Notify,
1208 Action::SetTweak(Tweak::Sound("default".into())),
1209 Action::SetTweak(Tweak::Highlight(true)),
1210 ],
1211 rule_id: ".m.rule.contains_user_name".into(),
1212 pattern: "user_id".into(),
1213 enabled: true,
1214 default: true,
1215 });
1216
1217 let set_value: JsonValue = to_json_value(set).unwrap();
1218 assert_eq!(
1219 set_value,
1220 json!({
1221 "override": [
1222 {
1223 "actions": [
1224 "notify",
1225 {
1226 "set_tweak": "highlight",
1227 },
1228 ],
1229 "conditions": [
1230 {
1231 "kind": "event_match",
1232 "key": "type",
1233 "pattern": "m.call.invite"
1234 },
1235 ],
1236 "rule_id": ".m.rule.call",
1237 "default": true,
1238 "enabled": true,
1239 },
1240 {
1241 "conditions": [
1242 {
1243 "kind": "room_member_count",
1244 "is": "2"
1245 },
1246 {
1247 "kind": "event_match",
1248 "key": "type",
1249 "pattern": "m.room.message"
1250 }
1251 ],
1252 "actions": [
1253 "notify",
1254 {
1255 "set_tweak": "sound",
1256 "value": "default"
1257 },
1258 {
1259 "set_tweak": "highlight",
1260 "value": false
1261 }
1262 ],
1263 "rule_id": ".m.rule.room_one_to_one",
1264 "default": true,
1265 "enabled": true
1266 },
1267 ],
1268 "content": [
1269 {
1270 "actions": [
1271 "notify",
1272 {
1273 "set_tweak": "sound",
1274 "value": "default"
1275 },
1276 {
1277 "set_tweak": "highlight"
1278 }
1279 ],
1280 "pattern": "user_id",
1281 "rule_id": ".m.rule.contains_user_name",
1282 "default": true,
1283 "enabled": true
1284 }
1285 ],
1286 })
1287 );
1288 }
1289
1290 #[test]
1291 fn deserialize_patterned_push_rule() {
1292 let rule = from_json_value::<PatternedPushRule>(json!({
1293 "actions": [
1294 "notify",
1295 {
1296 "set_tweak": "sound",
1297 "value": "default"
1298 },
1299 {
1300 "set_tweak": "highlight",
1301 "value": true
1302 }
1303 ],
1304 "pattern": "user_id",
1305 "rule_id": ".m.rule.contains_user_name",
1306 "default": true,
1307 "enabled": true
1308 }))
1309 .unwrap();
1310 assert!(rule.default);
1311 assert!(rule.enabled);
1312 assert_eq!(rule.pattern, "user_id");
1313 assert_eq!(rule.rule_id, ".m.rule.contains_user_name");
1314
1315 let mut iter = rule.actions.iter();
1316 assert_matches!(iter.next(), Some(Action::Notify));
1317 assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Sound(sound))));
1318 assert_eq!(sound, "default");
1319 assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Highlight(true))));
1320 assert_matches!(iter.next(), None);
1321 }
1322
1323 #[test]
1324 fn deserialize_ruleset() {
1325 let set: Ruleset = from_json_value(json!({
1326 "override": [
1327 {
1328 "actions": [],
1329 "conditions": [],
1330 "rule_id": "!roomid:server.name",
1331 "default": false,
1332 "enabled": true
1333 },
1334 {
1335 "actions": [],
1336 "conditions": [],
1337 "rule_id": ".m.rule.call",
1338 "default": true,
1339 "enabled": true
1340 },
1341 ],
1342 "underride": [
1343 {
1344 "actions": [],
1345 "conditions": [],
1346 "rule_id": ".m.rule.room_one_to_one",
1347 "default": true,
1348 "enabled": true
1349 },
1350 ],
1351 "room": [
1352 {
1353 "actions": [],
1354 "rule_id": "!roomid:server.name",
1355 "default": false,
1356 "enabled": false
1357 }
1358 ],
1359 "sender": [],
1360 "content": [
1361 {
1362 "actions": [],
1363 "pattern": "user_id",
1364 "rule_id": ".m.rule.contains_user_name",
1365 "default": true,
1366 "enabled": true
1367 },
1368 {
1369 "actions": [],
1370 "pattern": "ruma",
1371 "rule_id": "ruma",
1372 "default": false,
1373 "enabled": true
1374 }
1375 ]
1376 }))
1377 .unwrap();
1378
1379 let mut iter = set.into_iter();
1380
1381 let rule_opt = iter.next();
1382 assert!(rule_opt.is_some());
1383 assert_matches!(
1384 rule_opt.unwrap(),
1385 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1386 );
1387 assert_eq!(rule_id, "!roomid:server.name");
1388
1389 let rule_opt = iter.next();
1390 assert!(rule_opt.is_some());
1391 assert_matches!(
1392 rule_opt.unwrap(),
1393 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1394 );
1395 assert_eq!(rule_id, ".m.rule.call");
1396
1397 let rule_opt = iter.next();
1398 assert!(rule_opt.is_some());
1399 assert_matches!(rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }));
1400 assert_eq!(rule_id, ".m.rule.contains_user_name");
1401
1402 let rule_opt = iter.next();
1403 assert!(rule_opt.is_some());
1404 assert_matches!(rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }));
1405 assert_eq!(rule_id, "ruma");
1406
1407 let rule_opt = iter.next();
1408 assert!(rule_opt.is_some());
1409 assert_matches!(rule_opt.unwrap(), AnyPushRule::Room(SimplePushRule { rule_id, .. }));
1410 assert_eq!(rule_id, "!roomid:server.name");
1411
1412 let rule_opt = iter.next();
1413 assert!(rule_opt.is_some());
1414 assert_matches!(
1415 rule_opt.unwrap(),
1416 AnyPushRule::Underride(ConditionalPushRule { rule_id, .. })
1417 );
1418 assert_eq!(rule_id, ".m.rule.room_one_to_one");
1419
1420 assert_matches!(iter.next(), None);
1421 }
1422
1423 #[test]
1424 fn default_ruleset_applies() {
1425 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1426
1427 let context_one_to_one = &PushConditionRoomCtx {
1428 room_id: owned_room_id!("!dm:server.name"),
1429 member_count: uint!(2),
1430 user_id: owned_user_id!("@jj:server.name"),
1431 user_display_name: "Jolly Jumper".into(),
1432 power_levels: Some(power_levels()),
1433 #[cfg(feature = "unstable-msc3931")]
1434 supported_features: Default::default(),
1435 };
1436
1437 let context_public_room = &PushConditionRoomCtx {
1438 room_id: owned_room_id!("!far_west:server.name"),
1439 member_count: uint!(100),
1440 user_id: owned_user_id!("@jj:server.name"),
1441 user_display_name: "Jolly Jumper".into(),
1442 power_levels: Some(power_levels()),
1443 #[cfg(feature = "unstable-msc3931")]
1444 supported_features: Default::default(),
1445 };
1446
1447 let message = serde_json::from_str::<Raw<JsonValue>>(
1448 r#"{
1449 "type": "m.room.message"
1450 }"#,
1451 )
1452 .unwrap();
1453
1454 assert_matches!(
1455 set.get_actions(&message, context_one_to_one),
1456 [
1457 Action::Notify,
1458 Action::SetTweak(Tweak::Sound(_)),
1459 Action::SetTweak(Tweak::Highlight(false))
1460 ]
1461 );
1462 assert_matches!(
1463 set.get_actions(&message, context_public_room),
1464 [Action::Notify, Action::SetTweak(Tweak::Highlight(false))]
1465 );
1466
1467 let user_name = serde_json::from_str::<Raw<JsonValue>>(
1468 r#"{
1469 "type": "m.room.message",
1470 "content": {
1471 "body": "Hi jolly_jumper!"
1472 }
1473 }"#,
1474 )
1475 .unwrap();
1476
1477 assert_matches!(
1478 set.get_actions(&user_name, context_one_to_one),
1479 [
1480 Action::Notify,
1481 Action::SetTweak(Tweak::Sound(_)),
1482 Action::SetTweak(Tweak::Highlight(true)),
1483 ]
1484 );
1485 assert_matches!(
1486 set.get_actions(&user_name, context_public_room),
1487 [
1488 Action::Notify,
1489 Action::SetTweak(Tweak::Sound(_)),
1490 Action::SetTweak(Tweak::Highlight(true)),
1491 ]
1492 );
1493
1494 let notice = serde_json::from_str::<Raw<JsonValue>>(
1495 r#"{
1496 "type": "m.room.message",
1497 "content": {
1498 "msgtype": "m.notice"
1499 }
1500 }"#,
1501 )
1502 .unwrap();
1503 assert_matches!(set.get_actions(¬ice, context_one_to_one), []);
1504
1505 let at_room = serde_json::from_str::<Raw<JsonValue>>(
1506 r#"{
1507 "type": "m.room.message",
1508 "sender": "@rantanplan:server.name",
1509 "content": {
1510 "body": "@room Attention please!",
1511 "msgtype": "m.text"
1512 }
1513 }"#,
1514 )
1515 .unwrap();
1516
1517 assert_matches!(
1518 set.get_actions(&at_room, context_public_room),
1519 [Action::Notify, Action::SetTweak(Tweak::Highlight(true)),]
1520 );
1521
1522 let empty = serde_json::from_str::<Raw<JsonValue>>(r#"{}"#).unwrap();
1523 assert_matches!(set.get_actions(&empty, context_one_to_one), []);
1524 }
1525
1526 #[test]
1527 fn custom_ruleset_applies() {
1528 let context_one_to_one = &PushConditionRoomCtx {
1529 room_id: owned_room_id!("!dm:server.name"),
1530 member_count: uint!(2),
1531 user_id: owned_user_id!("@jj:server.name"),
1532 user_display_name: "Jolly Jumper".into(),
1533 power_levels: Some(power_levels()),
1534 #[cfg(feature = "unstable-msc3931")]
1535 supported_features: Default::default(),
1536 };
1537
1538 let message = serde_json::from_str::<Raw<JsonValue>>(
1539 r#"{
1540 "sender": "@rantanplan:server.name",
1541 "type": "m.room.message",
1542 "content": {
1543 "msgtype": "m.text",
1544 "body": "Great joke!"
1545 }
1546 }"#,
1547 )
1548 .unwrap();
1549
1550 let mut set = Ruleset::new();
1551 let disabled = ConditionalPushRule {
1552 actions: vec![Action::Notify],
1553 default: false,
1554 enabled: false,
1555 rule_id: "disabled".into(),
1556 conditions: vec![PushCondition::RoomMemberCount {
1557 is: RoomMemberCountIs::from(uint!(2)),
1558 }],
1559 };
1560 set.underride.insert(disabled);
1561
1562 let test_set = set.clone();
1563 assert_matches!(test_set.get_actions(&message, context_one_to_one), []);
1564
1565 let no_conditions = ConditionalPushRule {
1566 actions: vec![Action::SetTweak(Tweak::Highlight(true))],
1567 default: false,
1568 enabled: true,
1569 rule_id: "no.conditions".into(),
1570 conditions: vec![],
1571 };
1572 set.underride.insert(no_conditions);
1573
1574 let test_set = set.clone();
1575 assert_matches!(
1576 test_set.get_actions(&message, context_one_to_one),
1577 [Action::SetTweak(Tweak::Highlight(true))]
1578 );
1579
1580 let sender = SimplePushRule {
1581 actions: vec![Action::Notify],
1582 default: false,
1583 enabled: true,
1584 rule_id: owned_user_id!("@rantanplan:server.name"),
1585 };
1586 set.sender.insert(sender);
1587
1588 let test_set = set.clone();
1589 assert_matches!(test_set.get_actions(&message, context_one_to_one), [Action::Notify]);
1590
1591 let room = SimplePushRule {
1592 actions: vec![Action::SetTweak(Tweak::Highlight(true))],
1593 default: false,
1594 enabled: true,
1595 rule_id: owned_room_id!("!dm:server.name"),
1596 };
1597 set.room.insert(room);
1598
1599 let test_set = set.clone();
1600 assert_matches!(
1601 test_set.get_actions(&message, context_one_to_one),
1602 [Action::SetTweak(Tweak::Highlight(true))]
1603 );
1604
1605 let content = PatternedPushRule {
1606 actions: vec![Action::SetTweak(Tweak::Sound("content".into()))],
1607 default: false,
1608 enabled: true,
1609 rule_id: "content".into(),
1610 pattern: "joke".into(),
1611 };
1612 set.content.insert(content);
1613
1614 let test_set = set.clone();
1615 assert_matches!(
1616 test_set.get_actions(&message, context_one_to_one),
1617 [Action::SetTweak(Tweak::Sound(sound))]
1618 );
1619 assert_eq!(sound, "content");
1620
1621 let three_conditions = ConditionalPushRule {
1622 actions: vec![Action::SetTweak(Tweak::Sound("three".into()))],
1623 default: false,
1624 enabled: true,
1625 rule_id: "three.conditions".into(),
1626 conditions: vec![
1627 PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
1628 PushCondition::ContainsDisplayName,
1629 PushCondition::EventMatch {
1630 key: "room_id".into(),
1631 pattern: "!dm:server.name".into(),
1632 },
1633 ],
1634 };
1635 set.override_.insert(three_conditions);
1636
1637 assert_matches!(
1638 set.get_actions(&message, context_one_to_one),
1639 [Action::SetTweak(Tweak::Sound(sound))]
1640 );
1641 assert_eq!(sound, "content");
1642
1643 let new_message = serde_json::from_str::<Raw<JsonValue>>(
1644 r#"{
1645 "sender": "@rantanplan:server.name",
1646 "type": "m.room.message",
1647 "content": {
1648 "msgtype": "m.text",
1649 "body": "Tell me another one, Jolly Jumper!"
1650 }
1651 }"#,
1652 )
1653 .unwrap();
1654
1655 assert_matches!(
1656 set.get_actions(&new_message, context_one_to_one),
1657 [Action::SetTweak(Tweak::Sound(sound))]
1658 );
1659 assert_eq!(sound, "three");
1660 }
1661
1662 #[test]
1663 #[allow(deprecated)]
1664 fn old_mentions_apply() {
1665 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1666
1667 let context = &PushConditionRoomCtx {
1668 room_id: owned_room_id!("!far_west:server.name"),
1669 member_count: uint!(100),
1670 user_id: owned_user_id!("@jj:server.name"),
1671 user_display_name: "Jolly Jumper".into(),
1672 power_levels: Some(power_levels()),
1673 #[cfg(feature = "unstable-msc3931")]
1674 supported_features: Default::default(),
1675 };
1676
1677 let message = serde_json::from_str::<Raw<JsonValue>>(
1678 r#"{
1679 "content": {
1680 "body": "jolly_jumper"
1681 },
1682 "type": "m.room.message"
1683 }"#,
1684 )
1685 .unwrap();
1686
1687 assert_eq!(
1688 set.get_match(&message, context).unwrap().rule_id(),
1689 PredefinedContentRuleId::ContainsUserName.as_ref()
1690 );
1691
1692 let message = serde_json::from_str::<Raw<JsonValue>>(
1693 r#"{
1694 "content": {
1695 "body": "jolly_jumper",
1696 "m.mentions": {}
1697 },
1698 "type": "m.room.message"
1699 }"#,
1700 )
1701 .unwrap();
1702
1703 assert_ne!(
1704 set.get_match(&message, context).unwrap().rule_id(),
1705 PredefinedContentRuleId::ContainsUserName.as_ref()
1706 );
1707
1708 let message = serde_json::from_str::<Raw<JsonValue>>(
1709 r#"{
1710 "content": {
1711 "body": "Jolly Jumper"
1712 },
1713 "type": "m.room.message"
1714 }"#,
1715 )
1716 .unwrap();
1717
1718 assert_eq!(
1719 set.get_match(&message, context).unwrap().rule_id(),
1720 PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
1721 );
1722
1723 let message = serde_json::from_str::<Raw<JsonValue>>(
1724 r#"{
1725 "content": {
1726 "body": "Jolly Jumper",
1727 "m.mentions": {}
1728 },
1729 "type": "m.room.message"
1730 }"#,
1731 )
1732 .unwrap();
1733
1734 assert_ne!(
1735 set.get_match(&message, context).unwrap().rule_id(),
1736 PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
1737 );
1738
1739 let message = serde_json::from_str::<Raw<JsonValue>>(
1740 r#"{
1741 "content": {
1742 "body": "@room"
1743 },
1744 "sender": "@admin:server.name",
1745 "type": "m.room.message"
1746 }"#,
1747 )
1748 .unwrap();
1749
1750 assert_eq!(
1751 set.get_match(&message, context).unwrap().rule_id(),
1752 PredefinedOverrideRuleId::RoomNotif.as_ref()
1753 );
1754
1755 let message = serde_json::from_str::<Raw<JsonValue>>(
1756 r#"{
1757 "content": {
1758 "body": "@room",
1759 "m.mentions": {}
1760 },
1761 "sender": "@admin:server.name",
1762 "type": "m.room.message"
1763 }"#,
1764 )
1765 .unwrap();
1766
1767 assert_ne!(
1768 set.get_match(&message, context).unwrap().rule_id(),
1769 PredefinedOverrideRuleId::RoomNotif.as_ref()
1770 );
1771 }
1772
1773 #[test]
1774 fn intentional_mentions_apply() {
1775 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1776
1777 let context = &PushConditionRoomCtx {
1778 room_id: owned_room_id!("!far_west:server.name"),
1779 member_count: uint!(100),
1780 user_id: owned_user_id!("@jj:server.name"),
1781 user_display_name: "Jolly Jumper".into(),
1782 power_levels: Some(power_levels()),
1783 #[cfg(feature = "unstable-msc3931")]
1784 supported_features: Default::default(),
1785 };
1786
1787 let message = serde_json::from_str::<Raw<JsonValue>>(
1788 r#"{
1789 "content": {
1790 "body": "Hey jolly_jumper!",
1791 "m.mentions": {
1792 "user_ids": ["@jolly_jumper:server.name"]
1793 }
1794 },
1795 "sender": "@admin:server.name",
1796 "type": "m.room.message"
1797 }"#,
1798 )
1799 .unwrap();
1800
1801 assert_eq!(
1802 set.get_match(&message, context).unwrap().rule_id(),
1803 PredefinedOverrideRuleId::IsUserMention.as_ref()
1804 );
1805
1806 let message = serde_json::from_str::<Raw<JsonValue>>(
1807 r#"{
1808 "content": {
1809 "body": "Listen room!",
1810 "m.mentions": {
1811 "room": true
1812 }
1813 },
1814 "sender": "@admin:server.name",
1815 "type": "m.room.message"
1816 }"#,
1817 )
1818 .unwrap();
1819
1820 assert_eq!(
1821 set.get_match(&message, context).unwrap().rule_id(),
1822 PredefinedOverrideRuleId::IsRoomMention.as_ref()
1823 );
1824 }
1825
1826 #[test]
1827 fn invite_for_me_applies() {
1828 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1829
1830 let context = &PushConditionRoomCtx {
1831 room_id: owned_room_id!("!far_west:server.name"),
1832 member_count: uint!(100),
1833 user_id: owned_user_id!("@jj:server.name"),
1834 user_display_name: "Jolly Jumper".into(),
1835 power_levels: None,
1837 #[cfg(feature = "unstable-msc3931")]
1838 supported_features: Default::default(),
1839 };
1840
1841 let message = serde_json::from_str::<Raw<JsonValue>>(
1842 r#"{
1843 "content": {
1844 "membership": "invite"
1845 },
1846 "state_key": "@jolly_jumper:server.name",
1847 "sender": "@admin:server.name",
1848 "type": "m.room.member"
1849 }"#,
1850 )
1851 .unwrap();
1852
1853 assert_eq!(
1854 set.get_match(&message, context).unwrap().rule_id(),
1855 PredefinedOverrideRuleId::InviteForMe.as_ref()
1856 );
1857 }
1858}