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