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