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