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