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, value::RawValue as RawJsonValue,
1039 };
1040 use smol_macros::test;
1041
1042 use super::{
1043 AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule,
1044 action::{Action, Tweak},
1045 condition::{
1046 PushCondition, PushConditionPowerLevelsCtx, PushConditionRoomCtx, RoomMemberCountIs,
1047 },
1048 };
1049 use crate::{
1050 assert_to_canonical_json_eq, owned_room_id, owned_user_id,
1051 power_levels::NotificationPowerLevels,
1052 push::{PredefinedContentRuleId, PredefinedOverrideRuleId},
1053 room_version_rules::{AuthorizationRules, RoomPowerLevelsRules},
1054 serde::Raw,
1055 user_id,
1056 };
1057
1058 fn example_ruleset() -> Ruleset {
1059 let mut set = Ruleset::new();
1060
1061 set.override_.insert(ConditionalPushRule {
1062 conditions: vec![PushCondition::EventMatch {
1063 key: "type".into(),
1064 pattern: "m.call.invite".into(),
1065 }],
1066 actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1067 rule_id: ".m.rule.call".into(),
1068 enabled: true,
1069 default: true,
1070 });
1071
1072 set
1073 }
1074
1075 fn power_levels() -> PushConditionPowerLevelsCtx {
1076 PushConditionPowerLevelsCtx {
1077 users: BTreeMap::new(),
1078 users_default: int!(50),
1079 notifications: NotificationPowerLevels { room: int!(50) },
1080 rules: RoomPowerLevelsRules::new(&AuthorizationRules::V1, None),
1081 }
1082 }
1083
1084 static CONTEXT_ONE_TO_ONE: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1085 let mut ctx = PushConditionRoomCtx::new(
1086 owned_room_id!("!dm:server.name"),
1087 uint!(2),
1088 owned_user_id!("@jj:server.name"),
1089 "Jolly Jumper".into(),
1090 );
1091 ctx.power_levels = Some(power_levels());
1092 ctx
1093 });
1094
1095 static CONTEXT_PUBLIC_ROOM: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1096 let mut ctx = PushConditionRoomCtx::new(
1097 owned_room_id!("!far_west:server.name"),
1098 uint!(100),
1099 owned_user_id!("@jj:server.name"),
1100 "Jolly Jumper".into(),
1101 );
1102 ctx.power_levels = Some(power_levels());
1103 ctx
1104 });
1105
1106 #[test]
1107 fn iter() {
1108 let mut set = example_ruleset();
1109
1110 let added = set.override_.insert(ConditionalPushRule {
1111 conditions: vec![PushCondition::EventMatch {
1112 key: "room_id".into(),
1113 pattern: "!roomid:matrix.org".into(),
1114 }],
1115 actions: vec![],
1116 rule_id: "!roomid:matrix.org".into(),
1117 enabled: true,
1118 default: false,
1119 });
1120 assert!(added);
1121
1122 let added = set.override_.insert(ConditionalPushRule {
1123 conditions: vec![],
1124 actions: vec![],
1125 rule_id: ".m.rule.suppress_notices".into(),
1126 enabled: false,
1127 default: true,
1128 });
1129 assert!(added);
1130
1131 let mut iter = set.into_iter();
1132
1133 let rule_opt = iter.next();
1134 assert!(rule_opt.is_some());
1135 assert_matches!(
1136 rule_opt.unwrap(),
1137 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1138 );
1139 assert_eq!(rule_id, ".m.rule.call");
1140
1141 let rule_opt = iter.next();
1142 assert!(rule_opt.is_some());
1143 assert_matches!(
1144 rule_opt.unwrap(),
1145 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1146 );
1147 assert_eq!(rule_id, "!roomid:matrix.org");
1148
1149 let rule_opt = iter.next();
1150 assert!(rule_opt.is_some());
1151 assert_matches!(
1152 rule_opt.unwrap(),
1153 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1154 );
1155 assert_eq!(rule_id, ".m.rule.suppress_notices");
1156
1157 assert_matches!(iter.next(), None);
1158 }
1159
1160 #[test]
1161 fn serialize_conditional_push_rule() {
1162 let rule = ConditionalPushRule {
1163 actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1164 default: true,
1165 enabled: true,
1166 rule_id: ".m.rule.call".into(),
1167 conditions: vec![
1168 PushCondition::EventMatch { key: "type".into(), pattern: "m.call.invite".into() },
1169 #[allow(deprecated)]
1170 PushCondition::ContainsDisplayName,
1171 PushCondition::RoomMemberCount { is: RoomMemberCountIs::gt(uint!(2)) },
1172 PushCondition::SenderNotificationPermission { key: "room".into() },
1173 ],
1174 };
1175
1176 assert_to_canonical_json_eq!(
1177 rule,
1178 json!({
1179 "conditions": [
1180 {
1181 "kind": "event_match",
1182 "key": "type",
1183 "pattern": "m.call.invite"
1184 },
1185 {
1186 "kind": "contains_display_name"
1187 },
1188 {
1189 "kind": "room_member_count",
1190 "is": ">2"
1191 },
1192 {
1193 "kind": "sender_notification_permission",
1194 "key": "room"
1195 }
1196 ],
1197 "actions": [
1198 "notify",
1199 {
1200 "set_tweak": "highlight"
1201 }
1202 ],
1203 "rule_id": ".m.rule.call",
1204 "default": true,
1205 "enabled": true
1206 })
1207 );
1208 }
1209
1210 #[test]
1211 fn serialize_simple_push_rule() {
1212 let rule = SimplePushRule {
1213 actions: vec![Action::Notify],
1214 default: false,
1215 enabled: false,
1216 rule_id: owned_room_id!("!roomid:server.name"),
1217 };
1218
1219 assert_to_canonical_json_eq!(
1220 rule,
1221 json!({
1222 "actions": [
1223 "notify"
1224 ],
1225 "rule_id": "!roomid:server.name",
1226 "default": false,
1227 "enabled": false
1228 })
1229 );
1230 }
1231
1232 #[test]
1233 fn serialize_patterned_push_rule() {
1234 let rule = PatternedPushRule {
1235 actions: vec![
1236 Action::Notify,
1237 Action::SetTweak(Tweak::Sound("default".into())),
1238 Action::SetTweak(Tweak::Custom {
1239 name: "dance".into(),
1240 value: RawJsonValue::from_string("true".into()).unwrap(),
1241 }),
1242 ],
1243 default: true,
1244 enabled: true,
1245 pattern: "user_id".into(),
1246 rule_id: ".m.rule.contains_user_name".into(),
1247 };
1248
1249 assert_to_canonical_json_eq!(
1250 rule,
1251 json!({
1252 "actions": [
1253 "notify",
1254 {
1255 "set_tweak": "sound",
1256 "value": "default"
1257 },
1258 {
1259 "set_tweak": "dance",
1260 "value": true
1261 }
1262 ],
1263 "pattern": "user_id",
1264 "rule_id": ".m.rule.contains_user_name",
1265 "default": true,
1266 "enabled": true
1267 })
1268 );
1269 }
1270
1271 #[test]
1272 fn serialize_ruleset() {
1273 let mut set = example_ruleset();
1274
1275 set.override_.insert(ConditionalPushRule {
1276 conditions: vec![
1277 PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
1278 PushCondition::EventMatch { key: "type".into(), pattern: "m.room.message".into() },
1279 ],
1280 actions: vec![
1281 Action::Notify,
1282 Action::SetTweak(Tweak::Sound("default".into())),
1283 Action::SetTweak(Tweak::Highlight(false)),
1284 ],
1285 rule_id: ".m.rule.room_one_to_one".into(),
1286 enabled: true,
1287 default: true,
1288 });
1289 set.content.insert(PatternedPushRule {
1290 actions: vec![
1291 Action::Notify,
1292 Action::SetTweak(Tweak::Sound("default".into())),
1293 Action::SetTweak(Tweak::Highlight(true)),
1294 ],
1295 rule_id: ".m.rule.contains_user_name".into(),
1296 pattern: "user_id".into(),
1297 enabled: true,
1298 default: true,
1299 });
1300
1301 assert_to_canonical_json_eq!(
1302 set,
1303 json!({
1304 "override": [
1305 {
1306 "actions": [
1307 "notify",
1308 {
1309 "set_tweak": "highlight",
1310 },
1311 ],
1312 "conditions": [
1313 {
1314 "kind": "event_match",
1315 "key": "type",
1316 "pattern": "m.call.invite"
1317 },
1318 ],
1319 "rule_id": ".m.rule.call",
1320 "default": true,
1321 "enabled": true,
1322 },
1323 {
1324 "conditions": [
1325 {
1326 "kind": "room_member_count",
1327 "is": "2"
1328 },
1329 {
1330 "kind": "event_match",
1331 "key": "type",
1332 "pattern": "m.room.message"
1333 }
1334 ],
1335 "actions": [
1336 "notify",
1337 {
1338 "set_tweak": "sound",
1339 "value": "default"
1340 },
1341 {
1342 "set_tweak": "highlight",
1343 "value": false
1344 }
1345 ],
1346 "rule_id": ".m.rule.room_one_to_one",
1347 "default": true,
1348 "enabled": true
1349 },
1350 ],
1351 "content": [
1352 {
1353 "actions": [
1354 "notify",
1355 {
1356 "set_tweak": "sound",
1357 "value": "default"
1358 },
1359 {
1360 "set_tweak": "highlight"
1361 }
1362 ],
1363 "pattern": "user_id",
1364 "rule_id": ".m.rule.contains_user_name",
1365 "default": true,
1366 "enabled": true
1367 }
1368 ],
1369 })
1370 );
1371 }
1372
1373 #[test]
1374 fn deserialize_patterned_push_rule() {
1375 let rule = from_json_value::<PatternedPushRule>(json!({
1376 "actions": [
1377 "notify",
1378 {
1379 "set_tweak": "sound",
1380 "value": "default"
1381 },
1382 {
1383 "set_tweak": "highlight",
1384 "value": true
1385 }
1386 ],
1387 "pattern": "user_id",
1388 "rule_id": ".m.rule.contains_user_name",
1389 "default": true,
1390 "enabled": true
1391 }))
1392 .unwrap();
1393 assert!(rule.default);
1394 assert!(rule.enabled);
1395 assert_eq!(rule.pattern, "user_id");
1396 assert_eq!(rule.rule_id, ".m.rule.contains_user_name");
1397
1398 let mut iter = rule.actions.iter();
1399 assert_matches!(iter.next(), Some(Action::Notify));
1400 assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Sound(sound))));
1401 assert_eq!(sound, "default");
1402 assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Highlight(true))));
1403 assert_matches!(iter.next(), None);
1404 }
1405
1406 #[test]
1407 fn deserialize_ruleset() {
1408 let set: Ruleset = from_json_value(json!({
1409 "override": [
1410 {
1411 "actions": [],
1412 "conditions": [],
1413 "rule_id": "!roomid:server.name",
1414 "default": false,
1415 "enabled": true
1416 },
1417 {
1418 "actions": [],
1419 "conditions": [],
1420 "rule_id": ".m.rule.call",
1421 "default": true,
1422 "enabled": true
1423 },
1424 ],
1425 "underride": [
1426 {
1427 "actions": [],
1428 "conditions": [],
1429 "rule_id": ".m.rule.room_one_to_one",
1430 "default": true,
1431 "enabled": true
1432 },
1433 ],
1434 "room": [
1435 {
1436 "actions": [],
1437 "rule_id": "!roomid:server.name",
1438 "default": false,
1439 "enabled": false
1440 }
1441 ],
1442 "sender": [],
1443 "content": [
1444 {
1445 "actions": [],
1446 "pattern": "user_id",
1447 "rule_id": ".m.rule.contains_user_name",
1448 "default": true,
1449 "enabled": true
1450 },
1451 {
1452 "actions": [],
1453 "pattern": "ruma",
1454 "rule_id": "ruma",
1455 "default": false,
1456 "enabled": true
1457 }
1458 ]
1459 }))
1460 .unwrap();
1461
1462 let mut iter = set.into_iter();
1463
1464 let rule_opt = iter.next();
1465 assert!(rule_opt.is_some());
1466 assert_matches!(
1467 rule_opt.unwrap(),
1468 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1469 );
1470 assert_eq!(rule_id, "!roomid:server.name");
1471
1472 let rule_opt = iter.next();
1473 assert!(rule_opt.is_some());
1474 assert_matches!(
1475 rule_opt.unwrap(),
1476 AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1477 );
1478 assert_eq!(rule_id, ".m.rule.call");
1479
1480 let rule_opt = iter.next();
1481 assert!(rule_opt.is_some());
1482 assert_matches!(rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }));
1483 assert_eq!(rule_id, ".m.rule.contains_user_name");
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, "ruma");
1489
1490 let rule_opt = iter.next();
1491 assert!(rule_opt.is_some());
1492 assert_matches!(rule_opt.unwrap(), AnyPushRule::Room(SimplePushRule { rule_id, .. }));
1493 assert_eq!(rule_id, "!roomid:server.name");
1494
1495 let rule_opt = iter.next();
1496 assert!(rule_opt.is_some());
1497 assert_matches!(
1498 rule_opt.unwrap(),
1499 AnyPushRule::Underride(ConditionalPushRule { rule_id, .. })
1500 );
1501 assert_eq!(rule_id, ".m.rule.room_one_to_one");
1502
1503 assert_matches!(iter.next(), None);
1504 }
1505
1506 #[apply(test!)]
1507 async fn default_ruleset_applies() {
1508 let set = Ruleset::server_default(user_id!("@jj:server.name"));
1509
1510 let message = serde_json::from_str::<Raw<JsonValue>>(
1511 r#"{
1512 "type": "m.room.message"
1513 }"#,
1514 )
1515 .unwrap();
1516
1517 assert_matches!(
1518 set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1519 [
1520 Action::Notify,
1521 Action::SetTweak(Tweak::Sound(_)),
1522 Action::SetTweak(Tweak::Highlight(false))
1523 ]
1524 );
1525 assert_matches!(
1526 set.get_actions(&message, &CONTEXT_PUBLIC_ROOM).await,
1527 [Action::Notify, Action::SetTweak(Tweak::Highlight(false))]
1528 );
1529
1530 let user_mention = serde_json::from_str::<Raw<JsonValue>>(
1531 r#"{
1532 "type": "m.room.message",
1533 "content": {
1534 "body": "Hi jolly_jumper!",
1535 "m.mentions": {
1536 "user_ids": ["@jj:server.name"]
1537 }
1538 }
1539 }"#,
1540 )
1541 .unwrap();
1542
1543 assert_matches!(
1544 set.get_actions(&user_mention, &CONTEXT_ONE_TO_ONE).await,
1545 [
1546 Action::Notify,
1547 Action::SetTweak(Tweak::Sound(_)),
1548 Action::SetTweak(Tweak::Highlight(true)),
1549 ]
1550 );
1551 assert_matches!(
1552 set.get_actions(&user_mention, &CONTEXT_PUBLIC_ROOM).await,
1553 [
1554 Action::Notify,
1555 Action::SetTweak(Tweak::Sound(_)),
1556 Action::SetTweak(Tweak::Highlight(true)),
1557 ]
1558 );
1559
1560 let notice = serde_json::from_str::<Raw<JsonValue>>(
1561 r#"{
1562 "type": "m.room.message",
1563 "content": {
1564 "msgtype": "m.notice"
1565 }
1566 }"#,
1567 )
1568 .unwrap();
1569 assert_matches!(set.get_actions(¬ice, &CONTEXT_ONE_TO_ONE).await, []);
1570
1571 let room_mention = serde_json::from_str::<Raw<JsonValue>>(
1572 r#"{
1573 "type": "m.room.message",
1574 "sender": "@rantanplan:server.name",
1575 "content": {
1576 "body": "@room Attention please!",
1577 "msgtype": "m.text",
1578 "m.mentions": {
1579 "room": true
1580 }
1581 }
1582 }"#,
1583 )
1584 .unwrap();
1585
1586 assert_matches!(
1587 set.get_actions(&room_mention, &CONTEXT_PUBLIC_ROOM).await,
1588 [Action::Notify, Action::SetTweak(Tweak::Highlight(true)),]
1589 );
1590
1591 let empty = serde_json::from_str::<Raw<JsonValue>>(r#"{}"#).unwrap();
1592 assert_matches!(set.get_actions(&empty, &CONTEXT_ONE_TO_ONE).await, []);
1593 }
1594
1595 #[apply(test!)]
1596 async fn custom_ruleset_applies() {
1597 let message = serde_json::from_str::<Raw<JsonValue>>(
1598 r#"{
1599 "sender": "@rantanplan:server.name",
1600 "type": "m.room.message",
1601 "content": {
1602 "msgtype": "m.text",
1603 "body": "Great joke!"
1604 }
1605 }"#,
1606 )
1607 .unwrap();
1608
1609 let mut set = Ruleset::new();
1610 let disabled = ConditionalPushRule {
1611 actions: vec![Action::Notify],
1612 default: false,
1613 enabled: false,
1614 rule_id: "disabled".into(),
1615 conditions: vec![PushCondition::RoomMemberCount {
1616 is: RoomMemberCountIs::from(uint!(2)),
1617 }],
1618 };
1619 set.underride.insert(disabled);
1620
1621 let test_set = set.clone();
1622 assert_matches!(test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await, []);
1623
1624 let no_conditions = ConditionalPushRule {
1625 actions: vec![Action::SetTweak(Tweak::Highlight(true))],
1626 default: false,
1627 enabled: true,
1628 rule_id: "no.conditions".into(),
1629 conditions: vec![],
1630 };
1631 set.underride.insert(no_conditions);
1632
1633 let test_set = set.clone();
1634 assert_matches!(
1635 test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1636 [Action::SetTweak(Tweak::Highlight(true))]
1637 );
1638
1639 let sender = SimplePushRule {
1640 actions: vec![Action::Notify],
1641 default: false,
1642 enabled: true,
1643 rule_id: owned_user_id!("@rantanplan:server.name"),
1644 };
1645 set.sender.insert(sender);
1646
1647 let test_set = set.clone();
1648 assert_matches!(
1649 test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1650 [Action::Notify]
1651 );
1652
1653 let room = SimplePushRule {
1654 actions: vec![Action::SetTweak(Tweak::Highlight(true))],
1655 default: false,
1656 enabled: true,
1657 rule_id: owned_room_id!("!dm:server.name"),
1658 };
1659 set.room.insert(room);
1660
1661 let test_set = set.clone();
1662 assert_matches!(
1663 test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1664 [Action::SetTweak(Tweak::Highlight(true))]
1665 );
1666
1667 let content = PatternedPushRule {
1668 actions: vec![Action::SetTweak(Tweak::Sound("content".into()))],
1669 default: false,
1670 enabled: true,
1671 rule_id: "content".into(),
1672 pattern: "joke".into(),
1673 };
1674 set.content.insert(content);
1675
1676 let test_set = set.clone();
1677 assert_matches!(
1678 test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1679 [Action::SetTweak(Tweak::Sound(sound))]
1680 );
1681 assert_eq!(sound, "content");
1682
1683 let three_conditions = ConditionalPushRule {
1684 actions: vec![Action::SetTweak(Tweak::Sound("three".into()))],
1685 default: false,
1686 enabled: true,
1687 rule_id: "three.conditions".into(),
1688 conditions: vec![
1689 PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
1690 #[allow(deprecated)]
1691 PushCondition::ContainsDisplayName,
1692 PushCondition::EventMatch {
1693 key: "room_id".into(),
1694 pattern: "!dm:server.name".into(),
1695 },
1696 ],
1697 };
1698 set.override_.insert(three_conditions);
1699
1700 assert_matches!(
1701 set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1702 [Action::SetTweak(Tweak::Sound(sound))]
1703 );
1704 assert_eq!(sound, "content");
1705
1706 let new_message = serde_json::from_str::<Raw<JsonValue>>(
1707 r#"{
1708 "sender": "@rantanplan:server.name",
1709 "type": "m.room.message",
1710 "content": {
1711 "msgtype": "m.text",
1712 "body": "Tell me another one, Jolly Jumper!"
1713 }
1714 }"#,
1715 )
1716 .unwrap();
1717
1718 assert_matches!(
1719 set.get_actions(&new_message, &CONTEXT_ONE_TO_ONE).await,
1720 [Action::SetTweak(Tweak::Sound(sound))]
1721 );
1722 assert_eq!(sound, "three");
1723 }
1724
1725 #[apply(test!)]
1726 #[allow(deprecated)]
1727 async fn old_mentions_apply() {
1728 let mut set = Ruleset::new();
1729 set.content.insert(PatternedPushRule {
1730 rule_id: PredefinedContentRuleId::ContainsUserName.to_string(),
1731 enabled: true,
1732 default: true,
1733 pattern: "jolly_jumper".to_owned(),
1734 actions: vec![
1735 Action::Notify,
1736 Action::SetTweak(Tweak::Sound("default".into())),
1737 Action::SetTweak(Tweak::Highlight(true)),
1738 ],
1739 });
1740 set.override_.extend([
1741 ConditionalPushRule {
1742 actions: vec![
1743 Action::Notify,
1744 Action::SetTweak(Tweak::Sound("default".into())),
1745 Action::SetTweak(Tweak::Highlight(true)),
1746 ],
1747 default: true,
1748 enabled: true,
1749 rule_id: PredefinedOverrideRuleId::ContainsDisplayName.to_string(),
1750 conditions: vec![PushCondition::ContainsDisplayName],
1751 },
1752 ConditionalPushRule {
1753 actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1754 default: true,
1755 enabled: true,
1756 rule_id: PredefinedOverrideRuleId::RoomNotif.to_string(),
1757 conditions: vec![
1758 PushCondition::EventMatch {
1759 key: "content.body".into(),
1760 pattern: "@room".into(),
1761 },
1762 PushCondition::SenderNotificationPermission { key: "room".into() },
1763 ],
1764 },
1765 ]);
1766
1767 let message = serde_json::from_str::<Raw<JsonValue>>(
1768 r#"{
1769 "content": {
1770 "body": "jolly_jumper"
1771 },
1772 "type": "m.room.message"
1773 }"#,
1774 )
1775 .unwrap();
1776
1777 assert_eq!(
1778 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1779 PredefinedContentRuleId::ContainsUserName.as_ref()
1780 );
1781
1782 let message = serde_json::from_str::<Raw<JsonValue>>(
1783 r#"{
1784 "content": {
1785 "body": "jolly_jumper",
1786 "m.mentions": {}
1787 },
1788 "type": "m.room.message"
1789 }"#,
1790 )
1791 .unwrap();
1792
1793 assert_eq!(
1794 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1795 None
1796 );
1797
1798 let message = serde_json::from_str::<Raw<JsonValue>>(
1799 r#"{
1800 "content": {
1801 "body": "Jolly Jumper"
1802 },
1803 "type": "m.room.message"
1804 }"#,
1805 )
1806 .unwrap();
1807
1808 assert_eq!(
1809 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1810 PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
1811 );
1812
1813 let message = serde_json::from_str::<Raw<JsonValue>>(
1814 r#"{
1815 "content": {
1816 "body": "Jolly Jumper",
1817 "m.mentions": {}
1818 },
1819 "type": "m.room.message"
1820 }"#,
1821 )
1822 .unwrap();
1823
1824 assert_eq!(
1825 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1826 None
1827 );
1828
1829 let message = serde_json::from_str::<Raw<JsonValue>>(
1830 r#"{
1831 "content": {
1832 "body": "@room"
1833 },
1834 "sender": "@admin:server.name",
1835 "type": "m.room.message"
1836 }"#,
1837 )
1838 .unwrap();
1839
1840 assert_eq!(
1841 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1842 PredefinedOverrideRuleId::RoomNotif.as_ref()
1843 );
1844
1845 let message = serde_json::from_str::<Raw<JsonValue>>(
1846 r#"{
1847 "content": {
1848 "body": "@room",
1849 "m.mentions": {}
1850 },
1851 "sender": "@admin:server.name",
1852 "type": "m.room.message"
1853 }"#,
1854 )
1855 .unwrap();
1856
1857 assert_eq!(
1858 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1859 None
1860 );
1861 }
1862
1863 #[apply(test!)]
1864 async fn intentional_mentions_apply() {
1865 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1866
1867 let message = serde_json::from_str::<Raw<JsonValue>>(
1868 r#"{
1869 "content": {
1870 "body": "Hey jolly_jumper!",
1871 "m.mentions": {
1872 "user_ids": ["@jolly_jumper:server.name"]
1873 }
1874 },
1875 "sender": "@admin:server.name",
1876 "type": "m.room.message"
1877 }"#,
1878 )
1879 .unwrap();
1880
1881 assert_eq!(
1882 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1883 PredefinedOverrideRuleId::IsUserMention.as_ref()
1884 );
1885
1886 let message = serde_json::from_str::<Raw<JsonValue>>(
1887 r#"{
1888 "content": {
1889 "body": "Listen room!",
1890 "m.mentions": {
1891 "room": true
1892 }
1893 },
1894 "sender": "@admin:server.name",
1895 "type": "m.room.message"
1896 }"#,
1897 )
1898 .unwrap();
1899
1900 assert_eq!(
1901 set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1902 PredefinedOverrideRuleId::IsRoomMention.as_ref()
1903 );
1904 }
1905
1906 #[apply(test!)]
1907 async fn invite_for_me_applies() {
1908 let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1909
1910 let context = PushConditionRoomCtx::new(
1912 owned_room_id!("!far_west:server.name"),
1913 uint!(100),
1914 owned_user_id!("@jj:server.name"),
1915 "Jolly Jumper".into(),
1916 );
1917
1918 let message = serde_json::from_str::<Raw<JsonValue>>(
1919 r#"{
1920 "content": {
1921 "membership": "invite"
1922 },
1923 "state_key": "@jolly_jumper:server.name",
1924 "sender": "@admin:server.name",
1925 "type": "m.room.member"
1926 }"#,
1927 )
1928 .unwrap();
1929
1930 assert_eq!(
1931 set.get_match(&message, &context).await.unwrap().rule_id(),
1932 PredefinedOverrideRuleId::InviteForMe.as_ref()
1933 );
1934 }
1935}