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 async 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 return None;
300 }
301
302 for rule in self {
303 if rule.applies(&event, context).await {
304 return Some(rule);
305 }
306 }
307
308 None
309 }
310
311 #[instrument(skip_all, fields(context.room_id = %context.room_id))]
320 pub async fn get_actions<T>(
321 &self,
322 event: &Raw<T>,
323 context: &PushConditionRoomCtx,
324 ) -> &[Action] {
325 self.get_match(event, context).await.map(|rule| rule.actions()).unwrap_or(&[])
326 }
327
328 pub fn remove(
332 &mut self,
333 kind: RuleKind,
334 rule_id: impl AsRef<str>,
335 ) -> Result<(), RemovePushRuleError> {
336 let rule_id = rule_id.as_ref();
337
338 if let Some(rule) = self.get(kind.clone(), rule_id) {
339 if rule.is_server_default() {
340 return Err(RemovePushRuleError::ServerDefault);
341 }
342 } else {
343 return Err(RemovePushRuleError::NotFound);
344 }
345
346 match kind {
347 RuleKind::Override => {
348 self.override_.shift_remove(rule_id);
349 }
350 RuleKind::Underride => {
351 self.underride.shift_remove(rule_id);
352 }
353 RuleKind::Sender => {
354 self.sender.shift_remove(rule_id);
355 }
356 RuleKind::Room => {
357 self.room.shift_remove(rule_id);
358 }
359 RuleKind::Content => {
360 self.content.shift_remove(rule_id);
361 }
362 RuleKind::_Custom(_) => unreachable!(),
364 }
365
366 Ok(())
367 }
368}
369
370#[derive(Clone, Debug, Deserialize, Serialize)]
379#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
380pub struct SimplePushRule<T> {
381 pub actions: Vec<Action>,
383
384 pub default: bool,
386
387 pub enabled: bool,
389
390 pub rule_id: T,
394}
395
396#[derive(Debug)]
401#[allow(clippy::exhaustive_structs)]
402pub struct SimplePushRuleInit<T> {
403 pub actions: Vec<Action>,
405
406 pub default: bool,
408
409 pub enabled: bool,
411
412 pub rule_id: T,
416}
417
418impl<T> From<SimplePushRuleInit<T>> for SimplePushRule<T> {
419 fn from(init: SimplePushRuleInit<T>) -> Self {
420 let SimplePushRuleInit { actions, default, enabled, rule_id } = init;
421 Self { actions, default, enabled, rule_id }
422 }
423}
424
425impl<T> Hash for SimplePushRule<T>
429where
430 T: Hash,
431{
432 fn hash<H: Hasher>(&self, state: &mut H) {
433 self.rule_id.hash(state);
434 }
435}
436
437impl<T> PartialEq for SimplePushRule<T>
438where
439 T: PartialEq<T>,
440{
441 fn eq(&self, other: &Self) -> bool {
442 self.rule_id == other.rule_id
443 }
444}
445
446impl<T> Eq for SimplePushRule<T> where T: Eq {}
447
448impl<T> Equivalent<SimplePushRule<T>> for str
449where
450 T: AsRef<str>,
451{
452 fn equivalent(&self, key: &SimplePushRule<T>) -> bool {
453 self == key.rule_id.as_ref()
454 }
455}
456
457#[derive(Clone, Debug, Deserialize, Serialize)]
464#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
465pub struct ConditionalPushRule {
466 pub actions: Vec<Action>,
468
469 pub default: bool,
471
472 pub enabled: bool,
474
475 pub rule_id: String,
477
478 #[serde(default)]
483 pub conditions: Vec<PushCondition>,
484}
485
486impl ConditionalPushRule {
487 pub async fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
494 if !self.enabled {
495 return false;
496 }
497
498 #[cfg(feature = "unstable-msc3932")]
499 {
500 #[allow(deprecated)]
502 if self.rule_id != PredefinedOverrideRuleId::Master.as_ref()
503 && self.rule_id != PredefinedOverrideRuleId::RoomNotif.as_ref()
504 && self.rule_id != PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
505 {
506 let room_supports_ext_ev =
510 context.supported_features.contains(&RoomVersionFeature::ExtensibleEvents);
511 let rule_has_room_version_supports = self.conditions.iter().any(|condition| {
512 matches!(condition, PushCondition::RoomVersionSupports { .. })
513 });
514
515 if room_supports_ext_ev && !rule_has_room_version_supports {
516 return false;
517 }
518 }
519 }
520
521 #[allow(deprecated)]
523 if (self.rule_id == PredefinedOverrideRuleId::RoomNotif.as_ref()
524 || self.rule_id == PredefinedOverrideRuleId::ContainsDisplayName.as_ref())
525 && event.contains_mentions()
526 {
527 return false;
528 }
529
530 for cond in &self.conditions {
531 if !cond.applies(event, context).await {
532 return false;
533 }
534 }
535 true
536 }
537}
538
539#[derive(Debug)]
544#[allow(clippy::exhaustive_structs)]
545pub struct ConditionalPushRuleInit {
546 pub actions: Vec<Action>,
548
549 pub default: bool,
551
552 pub enabled: bool,
554
555 pub rule_id: String,
557
558 pub conditions: Vec<PushCondition>,
563}
564
565impl From<ConditionalPushRuleInit> for ConditionalPushRule {
566 fn from(init: ConditionalPushRuleInit) -> Self {
567 let ConditionalPushRuleInit { actions, default, enabled, rule_id, conditions } = init;
568 Self { actions, default, enabled, rule_id, conditions }
569 }
570}
571
572impl Hash for ConditionalPushRule {
576 fn hash<H: Hasher>(&self, state: &mut H) {
577 self.rule_id.hash(state);
578 }
579}
580
581impl PartialEq for ConditionalPushRule {
582 fn eq(&self, other: &Self) -> bool {
583 self.rule_id == other.rule_id
584 }
585}
586
587impl Eq for ConditionalPushRule {}
588
589impl Equivalent<ConditionalPushRule> for str {
590 fn equivalent(&self, key: &ConditionalPushRule) -> bool {
591 self == key.rule_id
592 }
593}
594
595#[derive(Clone, Debug, Deserialize, Serialize)]
602#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
603pub struct PatternedPushRule {
604 pub actions: Vec<Action>,
606
607 pub default: bool,
609
610 pub enabled: bool,
612
613 pub rule_id: String,
615
616 pub pattern: String,
618}
619
620impl PatternedPushRule {
621 pub fn applies_to(
628 &self,
629 key: &str,
630 event: &FlattenedJson,
631 context: &PushConditionRoomCtx,
632 ) -> bool {
633 #[allow(deprecated)]
635 if self.rule_id == PredefinedContentRuleId::ContainsUserName.as_ref()
636 && event.contains_mentions()
637 {
638 return false;
639 }
640
641 if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
642 return false;
643 }
644
645 self.enabled && condition::check_event_match(event, key, &self.pattern, context)
646 }
647}
648
649#[derive(Debug)]
654#[allow(clippy::exhaustive_structs)]
655pub struct PatternedPushRuleInit {
656 pub actions: Vec<Action>,
658
659 pub default: bool,
661
662 pub enabled: bool,
664
665 pub rule_id: String,
667
668 pub pattern: String,
670}
671
672impl From<PatternedPushRuleInit> for PatternedPushRule {
673 fn from(init: PatternedPushRuleInit) -> Self {
674 let PatternedPushRuleInit { actions, default, enabled, rule_id, pattern } = init;
675 Self { actions, default, enabled, rule_id, pattern }
676 }
677}
678
679impl Hash for PatternedPushRule {
683 fn hash<H: Hasher>(&self, state: &mut H) {
684 self.rule_id.hash(state);
685 }
686}
687
688impl PartialEq for PatternedPushRule {
689 fn eq(&self, other: &Self) -> bool {
690 self.rule_id == other.rule_id
691 }
692}
693
694impl Eq for PatternedPushRule {}
695
696impl Equivalent<PatternedPushRule> for str {
697 fn equivalent(&self, key: &PatternedPushRule) -> bool {
698 self == key.rule_id
699 }
700}
701
702#[derive(Clone, Debug, Serialize, Deserialize)]
704#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
705pub struct HttpPusherData {
706 pub url: String,
710
711 #[serde(skip_serializing_if = "Option::is_none")]
713 pub format: Option<PushFormat>,
714
715 #[serde(flatten, default, skip_serializing_if = "JsonObject::is_empty")]
717 pub data: JsonObject,
718}
719
720impl HttpPusherData {
721 pub fn new(url: String) -> Self {
723 Self { url, format: None, data: JsonObject::default() }
724 }
725}
726
727#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
732#[derive(Clone, PartialEq, Eq, StringEnum)]
733#[ruma_enum(rename_all = "snake_case")]
734#[non_exhaustive]
735pub enum PushFormat {
736 EventIdOnly,
738
739 #[doc(hidden)]
740 _Custom(PrivOwnedStr),
741}
742
743#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
745#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
746#[ruma_enum(rename_all = "snake_case")]
747#[non_exhaustive]
748pub enum RuleKind {
749 Override,
751
752 Underride,
754
755 Sender,
757
758 Room,
760
761 Content,
763
764 #[doc(hidden)]
765 _Custom(PrivOwnedStr),
766}
767
768#[derive(Clone, Debug)]
770#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
771pub enum NewPushRule {
772 Override(NewConditionalPushRule),
774
775 Content(NewPatternedPushRule),
777
778 Room(NewSimplePushRule<OwnedRoomId>),
780
781 Sender(NewSimplePushRule<OwnedUserId>),
783
784 Underride(NewConditionalPushRule),
786}
787
788impl NewPushRule {
789 pub fn kind(&self) -> RuleKind {
791 match self {
792 NewPushRule::Override(_) => RuleKind::Override,
793 NewPushRule::Content(_) => RuleKind::Content,
794 NewPushRule::Room(_) => RuleKind::Room,
795 NewPushRule::Sender(_) => RuleKind::Sender,
796 NewPushRule::Underride(_) => RuleKind::Underride,
797 }
798 }
799
800 pub fn rule_id(&self) -> &str {
802 match self {
803 NewPushRule::Override(r) => &r.rule_id,
804 NewPushRule::Content(r) => &r.rule_id,
805 NewPushRule::Room(r) => r.rule_id.as_ref(),
806 NewPushRule::Sender(r) => r.rule_id.as_ref(),
807 NewPushRule::Underride(r) => &r.rule_id,
808 }
809 }
810}
811
812#[derive(Clone, Debug, Deserialize, Serialize)]
814#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
815pub struct NewSimplePushRule<T> {
816 pub rule_id: T,
820
821 pub actions: Vec<Action>,
824}
825
826impl<T> NewSimplePushRule<T> {
827 pub fn new(rule_id: T, actions: Vec<Action>) -> Self {
829 Self { rule_id, actions }
830 }
831}
832
833impl<T> From<NewSimplePushRule<T>> for SimplePushRule<T> {
834 fn from(new_rule: NewSimplePushRule<T>) -> Self {
835 let NewSimplePushRule { rule_id, actions } = new_rule;
836 Self { actions, default: false, enabled: true, rule_id }
837 }
838}
839
840#[derive(Clone, Debug, Deserialize, Serialize)]
842#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
843pub struct NewPatternedPushRule {
844 pub rule_id: String,
846
847 pub pattern: String,
849
850 pub actions: Vec<Action>,
853}
854
855impl NewPatternedPushRule {
856 pub fn new(rule_id: String, pattern: String, actions: Vec<Action>) -> Self {
858 Self { rule_id, pattern, actions }
859 }
860}
861
862impl From<NewPatternedPushRule> for PatternedPushRule {
863 fn from(new_rule: NewPatternedPushRule) -> Self {
864 let NewPatternedPushRule { rule_id, pattern, actions } = new_rule;
865 Self { actions, default: false, enabled: true, rule_id, pattern }
866 }
867}
868
869#[derive(Clone, Debug, Deserialize, Serialize)]
871#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
872pub struct NewConditionalPushRule {
873 pub rule_id: String,
875
876 #[serde(default)]
881 pub conditions: Vec<PushCondition>,
882
883 pub actions: Vec<Action>,
886}
887
888impl NewConditionalPushRule {
889 pub fn new(rule_id: String, conditions: Vec<PushCondition>, actions: Vec<Action>) -> Self {
891 Self { rule_id, conditions, actions }
892 }
893}
894
895impl From<NewConditionalPushRule> for ConditionalPushRule {
896 fn from(new_rule: NewConditionalPushRule) -> Self {
897 let NewConditionalPushRule { rule_id, conditions, actions } = new_rule;
898 Self { actions, default: false, enabled: true, rule_id, conditions }
899 }
900}
901
902#[derive(Debug, Error)]
904#[non_exhaustive]
905pub enum InsertPushRuleError {
906 #[error("rule IDs starting with a dot are reserved for server-default rules")]
908 ServerDefaultRuleId,
909
910 #[error("invalid rule ID")]
912 InvalidRuleId,
913
914 #[error("can't place rule relative to server-default rule")]
916 RelativeToServerDefaultRule,
917
918 #[error("The before or after rule could not be found")]
920 UnknownRuleId,
921
922 #[error("before has a higher priority than after")]
924 BeforeHigherThanAfter,
925}
926
927#[derive(Debug, Error)]
929#[non_exhaustive]
930#[error("The rule could not be found")]
931pub struct RuleNotFoundError;
932
933pub fn insert_and_move_rule<T>(
935 set: &mut IndexSet<T>,
936 rule: T,
937 default_position: usize,
938 after: Option<&str>,
939 before: Option<&str>,
940) -> Result<(), InsertPushRuleError>
941where
942 T: Hash + Eq,
943 str: Equivalent<T>,
944{
945 let (from, replaced) = set.replace_full(rule);
946
947 let mut to = default_position;
948
949 if let Some(rule_id) = after {
950 let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
951 to = idx + 1;
952 }
953 if let Some(rule_id) = before {
954 let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
955
956 if idx < to {
957 return Err(InsertPushRuleError::BeforeHigherThanAfter);
958 }
959
960 to = idx;
961 }
962
963 if replaced.is_none() || after.is_some() || before.is_some() {
965 set.move_index(from, to);
966 }
967
968 Ok(())
969}
970
971#[derive(Debug, Error)]
973#[non_exhaustive]
974pub enum RemovePushRuleError {
975 #[error("server-default rules cannot be removed")]
977 ServerDefault,
978
979 #[error("rule not found")]
981 NotFound,
982}
983
984#[cfg(test)]
985mod tests {
986 use std::{collections::BTreeMap, sync::LazyLock};
987
988 use assert_matches2::assert_matches;
989 use js_int::{int, uint};
990 use macro_rules_attribute::apply;
991 use serde_json::{
992 from_value as from_json_value, json, to_value as to_json_value,
993 value::RawValue as RawJsonValue, Value as JsonValue,
994 };
995 use smol_macros::test;
996
997 use super::{
998 action::{Action, Tweak},
999 condition::{
1000 PushCondition, PushConditionPowerLevelsCtx, PushConditionRoomCtx, RoomMemberCountIs,
1001 },
1002 AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule,
1003 };
1004 use crate::{
1005 owned_room_id, owned_user_id,
1006 power_levels::NotificationPowerLevels,
1007 push::{PredefinedContentRuleId, PredefinedOverrideRuleId},
1008 room_version_rules::{AuthorizationRules, RoomPowerLevelsRules},
1009 serde::Raw,
1010 user_id,
1011 };
1012
1013 fn example_ruleset() -> Ruleset {
1014 let mut set = Ruleset::new();
1015
1016 set.override_.insert(ConditionalPushRule {
1017 conditions: vec![PushCondition::EventMatch {
1018 key: "type".into(),
1019 pattern: "m.call.invite".into(),
1020 }],
1021 actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1022 rule_id: ".m.rule.call".into(),
1023 enabled: true,
1024 default: true,
1025 });
1026
1027 set
1028 }
1029
1030 fn power_levels() -> PushConditionPowerLevelsCtx {
1031 PushConditionPowerLevelsCtx {
1032 users: BTreeMap::new(),
1033 users_default: int!(50),
1034 notifications: NotificationPowerLevels { room: int!(50) },
1035 rules: RoomPowerLevelsRules::new(&AuthorizationRules::V1, None),
1036 }
1037 }
1038
1039 static CONTEXT_ONE_TO_ONE: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1040 let mut ctx = PushConditionRoomCtx::new(
1041 owned_room_id!("!dm:server.name"),
1042 uint!(2),
1043 owned_user_id!("@jj:server.name"),
1044 "Jolly Jumper".into(),
1045 );
1046 ctx.power_levels = Some(power_levels());
1047 ctx
1048 });
1049
1050 static CONTEXT_PUBLIC_ROOM: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1051 let mut ctx = PushConditionRoomCtx::new(
1052 owned_room_id!("!far_west:server.name"),
1053 uint!(100),
1054 owned_user_id!("@jj:server.name"),
1055 "Jolly Jumper".into(),
1056 );
1057 ctx.power_levels = Some(power_levels());
1058 ctx
1059 });
1060
1061 #[test]
1062 fn iter() {
1063 let mut set = example_ruleset();
1064
1065 let added = set.override_.insert(ConditionalPushRule {
1066 conditions: vec![PushCondition::EventMatch {
1067 key: "room_id".into(),
1068 pattern: "!roomid:matrix.org".into(),
1069 }],
1070 actions: vec![],
1071 rule_id: "!roomid:matrix.org".into(),
1072 enabled: true,
1073 default: false,
1074 });
1075 assert!(added);
1076
1077 let added = set.override_.insert(ConditionalPushRule {
1078 conditions: vec![],
1079 actions: vec![],
1080 rule_id: ".m.rule.suppress_notices".into(),
1081 enabled: false,
1082 default: true,
1083 });
1084 assert!(added);
1085
1086 let mut iter = set.into_iter();
1087
1088 let rule_opt = iter.next();
1089 assert!(rule_opt.is_some());
1090 assert_matches!(
1091 rule_opt.unwrap(),
1092 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1093 );
1094 assert_eq!(rule_id, ".m.rule.call");
1095
1096 let rule_opt = iter.next();
1097 assert!(rule_opt.is_some());
1098 assert_matches!(
1099 rule_opt.unwrap(),
1100 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1101 );
1102 assert_eq!(rule_id, "!roomid:matrix.org");
1103
1104 let rule_opt = iter.next();
1105 assert!(rule_opt.is_some());
1106 assert_matches!(
1107 rule_opt.unwrap(),
1108 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1109 );
1110 assert_eq!(rule_id, ".m.rule.suppress_notices");
1111
1112 assert_matches!(iter.next(), None);
1113 }
1114
1115 #[test]
1116 fn serialize_conditional_push_rule() {
1117 let rule = ConditionalPushRule {
1118 actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1119 default: true,
1120 enabled: true,
1121 rule_id: ".m.rule.call".into(),
1122 conditions: vec![
1123 PushCondition::EventMatch { key: "type".into(), pattern: "m.call.invite".into() },
1124 PushCondition::ContainsDisplayName,
1125 PushCondition::RoomMemberCount { is: RoomMemberCountIs::gt(uint!(2)) },
1126 PushCondition::SenderNotificationPermission { key: "room".into() },
1127 ],
1128 };
1129
1130 let rule_value: JsonValue = to_json_value(rule).unwrap();
1131 assert_eq!(
1132 rule_value,
1133 json!({
1134 "conditions": [
1135 {
1136 "kind": "event_match",
1137 "key": "type",
1138 "pattern": "m.call.invite"
1139 },
1140 {
1141 "kind": "contains_display_name"
1142 },
1143 {
1144 "kind": "room_member_count",
1145 "is": ">2"
1146 },
1147 {
1148 "kind": "sender_notification_permission",
1149 "key": "room"
1150 }
1151 ],
1152 "actions": [
1153 "notify",
1154 {
1155 "set_tweak": "highlight"
1156 }
1157 ],
1158 "rule_id": ".m.rule.call",
1159 "default": true,
1160 "enabled": true
1161 })
1162 );
1163 }
1164
1165 #[test]
1166 fn serialize_simple_push_rule() {
1167 let rule = SimplePushRule {
1168 actions: vec![Action::Notify],
1169 default: false,
1170 enabled: false,
1171 rule_id: owned_room_id!("!roomid:server.name"),
1172 };
1173
1174 let rule_value: JsonValue = to_json_value(rule).unwrap();
1175 assert_eq!(
1176 rule_value,
1177 json!({
1178 "actions": [
1179 "notify"
1180 ],
1181 "rule_id": "!roomid:server.name",
1182 "default": false,
1183 "enabled": false
1184 })
1185 );
1186 }
1187
1188 #[test]
1189 fn serialize_patterned_push_rule() {
1190 let rule = PatternedPushRule {
1191 actions: vec![
1192 Action::Notify,
1193 Action::SetTweak(Tweak::Sound("default".into())),
1194 Action::SetTweak(Tweak::Custom {
1195 name: "dance".into(),
1196 value: RawJsonValue::from_string("true".into()).unwrap(),
1197 }),
1198 ],
1199 default: true,
1200 enabled: true,
1201 pattern: "user_id".into(),
1202 rule_id: ".m.rule.contains_user_name".into(),
1203 };
1204
1205 let rule_value: JsonValue = to_json_value(rule).unwrap();
1206 assert_eq!(
1207 rule_value,
1208 json!({
1209 "actions": [
1210 "notify",
1211 {
1212 "set_tweak": "sound",
1213 "value": "default"
1214 },
1215 {
1216 "set_tweak": "dance",
1217 "value": true
1218 }
1219 ],
1220 "pattern": "user_id",
1221 "rule_id": ".m.rule.contains_user_name",
1222 "default": true,
1223 "enabled": true
1224 })
1225 );
1226 }
1227
1228 #[test]
1229 fn serialize_ruleset() {
1230 let mut set = example_ruleset();
1231
1232 set.override_.insert(ConditionalPushRule {
1233 conditions: vec![
1234 PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
1235 PushCondition::EventMatch { key: "type".into(), pattern: "m.room.message".into() },
1236 ],
1237 actions: vec![
1238 Action::Notify,
1239 Action::SetTweak(Tweak::Sound("default".into())),
1240 Action::SetTweak(Tweak::Highlight(false)),
1241 ],
1242 rule_id: ".m.rule.room_one_to_one".into(),
1243 enabled: true,
1244 default: true,
1245 });
1246 set.content.insert(PatternedPushRule {
1247 actions: vec![
1248 Action::Notify,
1249 Action::SetTweak(Tweak::Sound("default".into())),
1250 Action::SetTweak(Tweak::Highlight(true)),
1251 ],
1252 rule_id: ".m.rule.contains_user_name".into(),
1253 pattern: "user_id".into(),
1254 enabled: true,
1255 default: true,
1256 });
1257
1258 let set_value: JsonValue = to_json_value(set).unwrap();
1259 assert_eq!(
1260 set_value,
1261 json!({
1262 "override": [
1263 {
1264 "actions": [
1265 "notify",
1266 {
1267 "set_tweak": "highlight",
1268 },
1269 ],
1270 "conditions": [
1271 {
1272 "kind": "event_match",
1273 "key": "type",
1274 "pattern": "m.call.invite"
1275 },
1276 ],
1277 "rule_id": ".m.rule.call",
1278 "default": true,
1279 "enabled": true,
1280 },
1281 {
1282 "conditions": [
1283 {
1284 "kind": "room_member_count",
1285 "is": "2"
1286 },
1287 {
1288 "kind": "event_match",
1289 "key": "type",
1290 "pattern": "m.room.message"
1291 }
1292 ],
1293 "actions": [
1294 "notify",
1295 {
1296 "set_tweak": "sound",
1297 "value": "default"
1298 },
1299 {
1300 "set_tweak": "highlight",
1301 "value": false
1302 }
1303 ],
1304 "rule_id": ".m.rule.room_one_to_one",
1305 "default": true,
1306 "enabled": true
1307 },
1308 ],
1309 "content": [
1310 {
1311 "actions": [
1312 "notify",
1313 {
1314 "set_tweak": "sound",
1315 "value": "default"
1316 },
1317 {
1318 "set_tweak": "highlight"
1319 }
1320 ],
1321 "pattern": "user_id",
1322 "rule_id": ".m.rule.contains_user_name",
1323 "default": true,
1324 "enabled": true
1325 }
1326 ],
1327 })
1328 );
1329 }
1330
1331 #[test]
1332 fn deserialize_patterned_push_rule() {
1333 let rule = from_json_value::<PatternedPushRule>(json!({
1334 "actions": [
1335 "notify",
1336 {
1337 "set_tweak": "sound",
1338 "value": "default"
1339 },
1340 {
1341 "set_tweak": "highlight",
1342 "value": true
1343 }
1344 ],
1345 "pattern": "user_id",
1346 "rule_id": ".m.rule.contains_user_name",
1347 "default": true,
1348 "enabled": true
1349 }))
1350 .unwrap();
1351 assert!(rule.default);
1352 assert!(rule.enabled);
1353 assert_eq!(rule.pattern, "user_id");
1354 assert_eq!(rule.rule_id, ".m.rule.contains_user_name");
1355
1356 let mut iter = rule.actions.iter();
1357 assert_matches!(iter.next(), Some(Action::Notify));
1358 assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Sound(sound))));
1359 assert_eq!(sound, "default");
1360 assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Highlight(true))));
1361 assert_matches!(iter.next(), None);
1362 }
1363
1364 #[test]
1365 fn deserialize_ruleset() {
1366 let set: Ruleset = from_json_value(json!({
1367 "override": [
1368 {
1369 "actions": [],
1370 "conditions": [],
1371 "rule_id": "!roomid:server.name",
1372 "default": false,
1373 "enabled": true
1374 },
1375 {
1376 "actions": [],
1377 "conditions": [],
1378 "rule_id": ".m.rule.call",
1379 "default": true,
1380 "enabled": true
1381 },
1382 ],
1383 "underride": [
1384 {
1385 "actions": [],
1386 "conditions": [],
1387 "rule_id": ".m.rule.room_one_to_one",
1388 "default": true,
1389 "enabled": true
1390 },
1391 ],
1392 "room": [
1393 {
1394 "actions": [],
1395 "rule_id": "!roomid:server.name",
1396 "default": false,
1397 "enabled": false
1398 }
1399 ],
1400 "sender": [],
1401 "content": [
1402 {
1403 "actions": [],
1404 "pattern": "user_id",
1405 "rule_id": ".m.rule.contains_user_name",
1406 "default": true,
1407 "enabled": true
1408 },
1409 {
1410 "actions": [],
1411 "pattern": "ruma",
1412 "rule_id": "ruma",
1413 "default": false,
1414 "enabled": true
1415 }
1416 ]
1417 }))
1418 .unwrap();
1419
1420 let mut iter = set.into_iter();
1421
1422 let rule_opt = iter.next();
1423 assert!(rule_opt.is_some());
1424 assert_matches!(
1425 rule_opt.unwrap(),
1426 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1427 );
1428 assert_eq!(rule_id, "!roomid:server.name");
1429
1430 let rule_opt = iter.next();
1431 assert!(rule_opt.is_some());
1432 assert_matches!(
1433 rule_opt.unwrap(),
1434 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1435 );
1436 assert_eq!(rule_id, ".m.rule.call");
1437
1438 let rule_opt = iter.next();
1439 assert!(rule_opt.is_some());
1440 assert_matches!(rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }));
1441 assert_eq!(rule_id, ".m.rule.contains_user_name");
1442
1443 let rule_opt = iter.next();
1444 assert!(rule_opt.is_some());
1445 assert_matches!(rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }));
1446 assert_eq!(rule_id, "ruma");
1447
1448 let rule_opt = iter.next();
1449 assert!(rule_opt.is_some());
1450 assert_matches!(rule_opt.unwrap(), AnyPushRule::Room(SimplePushRule { rule_id, .. }));
1451 assert_eq!(rule_id, "!roomid:server.name");
1452
1453 let rule_opt = iter.next();
1454 assert!(rule_opt.is_some());
1455 assert_matches!(
1456 rule_opt.unwrap(),
1457 AnyPushRule::Underride(ConditionalPushRule { rule_id, .. })
1458 );
1459 assert_eq!(rule_id, ".m.rule.room_one_to_one");
1460
1461 assert_matches!(iter.next(), None);
1462 }
1463
1464 #[apply(test!)]
1465 async fn default_ruleset_applies() {
1466 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1467
1468 let message = serde_json::from_str::<Raw<JsonValue>>(
1469 r#"{
1470 "type": "m.room.message"
1471 }"#,
1472 )
1473 .unwrap();
1474
1475 assert_matches!(
1476 set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1477 [
1478 Action::Notify,
1479 Action::SetTweak(Tweak::Sound(_)),
1480 Action::SetTweak(Tweak::Highlight(false))
1481 ]
1482 );
1483 assert_matches!(
1484 set.get_actions(&message, &CONTEXT_PUBLIC_ROOM).await,
1485 [Action::Notify, Action::SetTweak(Tweak::Highlight(false))]
1486 );
1487
1488 let user_name = serde_json::from_str::<Raw<JsonValue>>(
1489 r#"{
1490 "type": "m.room.message",
1491 "content": {
1492 "body": "Hi jolly_jumper!"
1493 }
1494 }"#,
1495 )
1496 .unwrap();
1497
1498 assert_matches!(
1499 set.get_actions(&user_name, &CONTEXT_ONE_TO_ONE).await,
1500 [
1501 Action::Notify,
1502 Action::SetTweak(Tweak::Sound(_)),
1503 Action::SetTweak(Tweak::Highlight(true)),
1504 ]
1505 );
1506 assert_matches!(
1507 set.get_actions(&user_name, &CONTEXT_PUBLIC_ROOM).await,
1508 [
1509 Action::Notify,
1510 Action::SetTweak(Tweak::Sound(_)),
1511 Action::SetTweak(Tweak::Highlight(true)),
1512 ]
1513 );
1514
1515 let notice = serde_json::from_str::<Raw<JsonValue>>(
1516 r#"{
1517 "type": "m.room.message",
1518 "content": {
1519 "msgtype": "m.notice"
1520 }
1521 }"#,
1522 )
1523 .unwrap();
1524 assert_matches!(set.get_actions(¬ice, &CONTEXT_ONE_TO_ONE).await, []);
1525
1526 let at_room = serde_json::from_str::<Raw<JsonValue>>(
1527 r#"{
1528 "type": "m.room.message",
1529 "sender": "@rantanplan:server.name",
1530 "content": {
1531 "body": "@room Attention please!",
1532 "msgtype": "m.text"
1533 }
1534 }"#,
1535 )
1536 .unwrap();
1537
1538 assert_matches!(
1539 set.get_actions(&at_room, &CONTEXT_PUBLIC_ROOM).await,
1540 [Action::Notify, Action::SetTweak(Tweak::Highlight(true)),]
1541 );
1542
1543 let empty = serde_json::from_str::<Raw<JsonValue>>(r#"{}"#).unwrap();
1544 assert_matches!(set.get_actions(&empty, &CONTEXT_ONE_TO_ONE).await, []);
1545 }
1546
1547 #[apply(test!)]
1548 async fn custom_ruleset_applies() {
1549 let message = serde_json::from_str::<Raw<JsonValue>>(
1550 r#"{
1551 "sender": "@rantanplan:server.name",
1552 "type": "m.room.message",
1553 "content": {
1554 "msgtype": "m.text",
1555 "body": "Great joke!"
1556 }
1557 }"#,
1558 )
1559 .unwrap();
1560
1561 let mut set = Ruleset::new();
1562 let disabled = ConditionalPushRule {
1563 actions: vec![Action::Notify],
1564 default: false,
1565 enabled: false,
1566 rule_id: "disabled".into(),
1567 conditions: vec![PushCondition::RoomMemberCount {
1568 is: RoomMemberCountIs::from(uint!(2)),
1569 }],
1570 };
1571 set.underride.insert(disabled);
1572
1573 let test_set = set.clone();
1574 assert_matches!(test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await, []);
1575
1576 let no_conditions = ConditionalPushRule {
1577 actions: vec![Action::SetTweak(Tweak::Highlight(true))],
1578 default: false,
1579 enabled: true,
1580 rule_id: "no.conditions".into(),
1581 conditions: vec![],
1582 };
1583 set.underride.insert(no_conditions);
1584
1585 let test_set = set.clone();
1586 assert_matches!(
1587 test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1588 [Action::SetTweak(Tweak::Highlight(true))]
1589 );
1590
1591 let sender = SimplePushRule {
1592 actions: vec![Action::Notify],
1593 default: false,
1594 enabled: true,
1595 rule_id: owned_user_id!("@rantanplan:server.name"),
1596 };
1597 set.sender.insert(sender);
1598
1599 let test_set = set.clone();
1600 assert_matches!(
1601 test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1602 [Action::Notify]
1603 );
1604
1605 let room = SimplePushRule {
1606 actions: vec![Action::SetTweak(Tweak::Highlight(true))],
1607 default: false,
1608 enabled: true,
1609 rule_id: owned_room_id!("!dm:server.name"),
1610 };
1611 set.room.insert(room);
1612
1613 let test_set = set.clone();
1614 assert_matches!(
1615 test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1616 [Action::SetTweak(Tweak::Highlight(true))]
1617 );
1618
1619 let content = PatternedPushRule {
1620 actions: vec![Action::SetTweak(Tweak::Sound("content".into()))],
1621 default: false,
1622 enabled: true,
1623 rule_id: "content".into(),
1624 pattern: "joke".into(),
1625 };
1626 set.content.insert(content);
1627
1628 let test_set = set.clone();
1629 assert_matches!(
1630 test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1631 [Action::SetTweak(Tweak::Sound(sound))]
1632 );
1633 assert_eq!(sound, "content");
1634
1635 let three_conditions = ConditionalPushRule {
1636 actions: vec![Action::SetTweak(Tweak::Sound("three".into()))],
1637 default: false,
1638 enabled: true,
1639 rule_id: "three.conditions".into(),
1640 conditions: vec![
1641 PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
1642 PushCondition::ContainsDisplayName,
1643 PushCondition::EventMatch {
1644 key: "room_id".into(),
1645 pattern: "!dm:server.name".into(),
1646 },
1647 ],
1648 };
1649 set.override_.insert(three_conditions);
1650
1651 assert_matches!(
1652 set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1653 [Action::SetTweak(Tweak::Sound(sound))]
1654 );
1655 assert_eq!(sound, "content");
1656
1657 let new_message = serde_json::from_str::<Raw<JsonValue>>(
1658 r#"{
1659 "sender": "@rantanplan:server.name",
1660 "type": "m.room.message",
1661 "content": {
1662 "msgtype": "m.text",
1663 "body": "Tell me another one, Jolly Jumper!"
1664 }
1665 }"#,
1666 )
1667 .unwrap();
1668
1669 assert_matches!(
1670 set.get_actions(&new_message, &CONTEXT_ONE_TO_ONE).await,
1671 [Action::SetTweak(Tweak::Sound(sound))]
1672 );
1673 assert_eq!(sound, "three");
1674 }
1675
1676 #[apply(test!)]
1677 #[allow(deprecated)]
1678 async fn old_mentions_apply() {
1679 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1680
1681 let message = serde_json::from_str::<Raw<JsonValue>>(
1682 r#"{
1683 "content": {
1684 "body": "jolly_jumper"
1685 },
1686 "type": "m.room.message"
1687 }"#,
1688 )
1689 .unwrap();
1690
1691 assert_eq!(
1692 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1693 PredefinedContentRuleId::ContainsUserName.as_ref()
1694 );
1695
1696 let message = serde_json::from_str::<Raw<JsonValue>>(
1697 r#"{
1698 "content": {
1699 "body": "jolly_jumper",
1700 "m.mentions": {}
1701 },
1702 "type": "m.room.message"
1703 }"#,
1704 )
1705 .unwrap();
1706
1707 assert_ne!(
1708 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1709 PredefinedContentRuleId::ContainsUserName.as_ref()
1710 );
1711
1712 let message = serde_json::from_str::<Raw<JsonValue>>(
1713 r#"{
1714 "content": {
1715 "body": "Jolly Jumper"
1716 },
1717 "type": "m.room.message"
1718 }"#,
1719 )
1720 .unwrap();
1721
1722 assert_eq!(
1723 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1724 PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
1725 );
1726
1727 let message = serde_json::from_str::<Raw<JsonValue>>(
1728 r#"{
1729 "content": {
1730 "body": "Jolly Jumper",
1731 "m.mentions": {}
1732 },
1733 "type": "m.room.message"
1734 }"#,
1735 )
1736 .unwrap();
1737
1738 assert_ne!(
1739 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1740 PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
1741 );
1742
1743 let message = serde_json::from_str::<Raw<JsonValue>>(
1744 r#"{
1745 "content": {
1746 "body": "@room"
1747 },
1748 "sender": "@admin:server.name",
1749 "type": "m.room.message"
1750 }"#,
1751 )
1752 .unwrap();
1753
1754 assert_eq!(
1755 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1756 PredefinedOverrideRuleId::RoomNotif.as_ref()
1757 );
1758
1759 let message = serde_json::from_str::<Raw<JsonValue>>(
1760 r#"{
1761 "content": {
1762 "body": "@room",
1763 "m.mentions": {}
1764 },
1765 "sender": "@admin:server.name",
1766 "type": "m.room.message"
1767 }"#,
1768 )
1769 .unwrap();
1770
1771 assert_ne!(
1772 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1773 PredefinedOverrideRuleId::RoomNotif.as_ref()
1774 );
1775 }
1776
1777 #[apply(test!)]
1778 async fn intentional_mentions_apply() {
1779 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1780
1781 let message = serde_json::from_str::<Raw<JsonValue>>(
1782 r#"{
1783 "content": {
1784 "body": "Hey jolly_jumper!",
1785 "m.mentions": {
1786 "user_ids": ["@jolly_jumper:server.name"]
1787 }
1788 },
1789 "sender": "@admin:server.name",
1790 "type": "m.room.message"
1791 }"#,
1792 )
1793 .unwrap();
1794
1795 assert_eq!(
1796 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1797 PredefinedOverrideRuleId::IsUserMention.as_ref()
1798 );
1799
1800 let message = serde_json::from_str::<Raw<JsonValue>>(
1801 r#"{
1802 "content": {
1803 "body": "Listen room!",
1804 "m.mentions": {
1805 "room": true
1806 }
1807 },
1808 "sender": "@admin:server.name",
1809 "type": "m.room.message"
1810 }"#,
1811 )
1812 .unwrap();
1813
1814 assert_eq!(
1815 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1816 PredefinedOverrideRuleId::IsRoomMention.as_ref()
1817 );
1818 }
1819
1820 #[apply(test!)]
1821 async fn invite_for_me_applies() {
1822 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1823
1824 let context = PushConditionRoomCtx::new(
1826 owned_room_id!("!far_west:server.name"),
1827 uint!(100),
1828 owned_user_id!("@jj:server.name"),
1829 "Jolly Jumper".into(),
1830 );
1831
1832 let message = serde_json::from_str::<Raw<JsonValue>>(
1833 r#"{
1834 "content": {
1835 "membership": "invite"
1836 },
1837 "state_key": "@jolly_jumper:server.name",
1838 "sender": "@admin:server.name",
1839 "type": "m.room.member"
1840 }"#,
1841 )
1842 .unwrap();
1843
1844 assert_eq!(
1845 set.get_match(&message, &context).await.unwrap().rule_id(),
1846 PredefinedOverrideRuleId::InviteForMe.as_ref()
1847 );
1848 }
1849}