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