Skip to content

Commit 2b70923

Browse files
committed
feat: time and day push rule condition rust
1 parent 232adfb commit 2b70923

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed

changelog.d/16858.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experimental support for [MSC3767](https://github.com/matrix-org/matrix-spec-proposals/pull/3767): the `time_and_day` push rule condition. Contributed by @hanadi92.

rust/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pythonize = "0.20.0"
3636
regex = "1.6.0"
3737
serde = { version = "1.0.144", features = ["derive"] }
3838
serde_json = "1.0.85"
39+
chrono = "0.4.33"
3940

4041
[features]
4142
extension-module = ["pyo3/extension-module"]

rust/src/push/evaluator.rs

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use std::borrow::Cow;
2323
use std::collections::BTreeMap;
2424

2525
use anyhow::{Context, Error};
26+
use chrono::Datelike;
2627
use lazy_static::lazy_static;
2728
use log::warn;
2829
use pyo3::prelude::*;
@@ -31,7 +32,7 @@ use regex::Regex;
3132
use super::{
3233
utils::{get_glob_matcher, get_localpart_from_id, GlobMatchType},
3334
Action, Condition, EventPropertyIsCondition, FilteredPushRules, KnownCondition,
34-
SimpleJsonValue,
35+
SimpleJsonValue, TimeAndDayIntervals,
3536
};
3637
use crate::push::{EventMatchPatternType, JsonValue};
3738

@@ -105,6 +106,9 @@ pub struct PushRuleEvaluator {
105106
/// If MSC3931 (room version feature flags) is enabled. Usually controlled by the same
106107
/// flag as MSC1767 (extensible events core).
107108
msc3931_enabled: bool,
109+
110+
/// If MSC3767 (time based notification filtering push rule condition) is enabled
111+
msc3767_time_and_day: bool,
108112
}
109113

110114
#[pymethods]
@@ -122,6 +126,7 @@ impl PushRuleEvaluator {
122126
related_event_match_enabled,
123127
room_version_feature_flags,
124128
msc3931_enabled,
129+
msc3767_time_and_day,
125130
))]
126131
pub fn py_new(
127132
flattened_keys: BTreeMap<String, JsonValue>,
@@ -133,6 +138,7 @@ impl PushRuleEvaluator {
133138
related_event_match_enabled: bool,
134139
room_version_feature_flags: Vec<String>,
135140
msc3931_enabled: bool,
141+
msc3767_time_and_day: bool,
136142
) -> Result<Self, Error> {
137143
let body = match flattened_keys.get("content.body") {
138144
Some(JsonValue::Value(SimpleJsonValue::Str(s))) => s.clone().into_owned(),
@@ -150,6 +156,7 @@ impl PushRuleEvaluator {
150156
related_event_match_enabled,
151157
room_version_feature_flags,
152158
msc3931_enabled,
159+
msc3767_time_and_day,
153160
})
154161
}
155162

@@ -384,6 +391,13 @@ impl PushRuleEvaluator {
384391
&& self.room_version_feature_flags.contains(&flag)
385392
}
386393
}
394+
KnownCondition::TimeAndDay(time_and_day) => {
395+
if !self.msc3767_time_and_day {
396+
false
397+
} else {
398+
self.match_time_and_day(time_and_day.timezone.clone(), &time_and_day.intervals)?
399+
}
400+
}
387401
};
388402

389403
Ok(result)
@@ -507,6 +521,31 @@ impl PushRuleEvaluator {
507521

508522
Ok(matches)
509523
}
524+
525+
///
526+
fn match_time_and_day(
527+
&self,
528+
_timezone: Option<Cow<str>>,
529+
intervals: &[TimeAndDayIntervals],
530+
) -> Result<bool, Error> {
531+
// Temp Notes from spec:
532+
// The timezone to use for time comparison. This format allows for automatic DST handling.
533+
// Intervals representing time periods in which the rule should match. Evaluated with an OR condition.
534+
//
535+
// time_of_day condition is met when the server's timezone-adjusted time is between the values of the tuple,
536+
// or when no time_of_day is set on the interval. Values are inclusive.
537+
// day_of_week condition is met when the server's timezone-adjusted day is included in the array.
538+
// next step -> consider timezone if given
539+
let now = chrono::Utc::now();
540+
let today = now.weekday().num_days_from_sunday();
541+
let current_time = now.time().format("%H:%M").to_string();
542+
let matches = intervals.iter().any(|interval| {
543+
interval.day_of_week.contains(&today)
544+
&& interval.time_of_day.contains(current_time.to_string())
545+
});
546+
547+
Ok(matches)
548+
}
510549
}
511550

512551
#[test]
@@ -526,6 +565,7 @@ fn push_rule_evaluator() {
526565
true,
527566
vec![],
528567
true,
568+
true,
529569
)
530570
.unwrap();
531571

@@ -555,6 +595,7 @@ fn test_requires_room_version_supports_condition() {
555595
false,
556596
flags,
557597
true,
598+
true,
558599
)
559600
.unwrap();
560601

@@ -588,3 +629,72 @@ fn test_requires_room_version_supports_condition() {
588629
);
589630
assert_eq!(result.len(), 1);
590631
}
632+
633+
#[test]
634+
fn test_time_and_day_condition() {
635+
use chrono::Duration;
636+
637+
use crate::push::{PushRule, PushRules, TimeAndDayCondition, TimeInterval};
638+
639+
let mut flattened_keys = BTreeMap::new();
640+
flattened_keys.insert(
641+
"content.body".to_string(),
642+
JsonValue::Value(SimpleJsonValue::Str(Cow::Borrowed("foo bar bob hello"))),
643+
);
644+
let evaluator = PushRuleEvaluator::py_new(
645+
flattened_keys,
646+
false,
647+
10,
648+
Some(0),
649+
BTreeMap::new(),
650+
BTreeMap::new(),
651+
true,
652+
vec![],
653+
true,
654+
true,
655+
)
656+
.unwrap();
657+
658+
// smoke test: notify is working with other conditions
659+
let result = evaluator.run(
660+
&FilteredPushRules::default(),
661+
Some("@bob:example.org"),
662+
None,
663+
);
664+
assert_eq!(result.len(), 3);
665+
666+
// for testing sakes, use current duration of two hours behind and forward of now as dnd
667+
let test_time = chrono::Utc::now();
668+
let test_start_time = (test_time - Duration::hours(2)).format("%H:%M").to_string();
669+
let test_end_time = (test_time + Duration::hours(2)).format("%H:%M").to_string();
670+
671+
// time and day condition in push rule
672+
let custom_rule = PushRule {
673+
rule_id: Cow::from(".m.rule.master"),
674+
priority_class: 5, // override
675+
conditions: Cow::from(vec![Condition::Known(KnownCondition::TimeAndDay(
676+
TimeAndDayCondition {
677+
timezone: None,
678+
intervals: vec![TimeAndDayIntervals {
679+
time_of_day: TimeInterval {
680+
start_time: Cow::from(test_start_time),
681+
end_time: Cow::from(test_end_time),
682+
},
683+
day_of_week: vec![6],
684+
}],
685+
},
686+
))]),
687+
actions: Cow::from(vec![Action::DontNotify]),
688+
default: true,
689+
default_enabled: true,
690+
};
691+
let rules = PushRules::new(vec![custom_rule]);
692+
let result = evaluator.run(
693+
&FilteredPushRules::py_new(rules, BTreeMap::new(), true, false, true, false),
694+
None,
695+
None,
696+
);
697+
698+
// dnd time, dont_notify
699+
assert_eq!(result.len(), 0);
700+
}

rust/src/push/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,8 @@ pub enum KnownCondition {
361361
RoomVersionSupports {
362362
feature: Cow<'static, str>,
363363
},
364+
#[serde(rename = "org.matrix.msc3767.time_and_day")]
365+
TimeAndDay(TimeAndDayCondition),
364366
}
365367

366368
impl IntoPy<PyObject> for Condition {
@@ -438,6 +440,38 @@ pub struct RelatedEventMatchTypeCondition {
438440
pub include_fallbacks: Option<bool>,
439441
}
440442

443+
/// The body of [`KnownCondition::TimeAndDay`]
444+
#[derive(Serialize, Deserialize, Debug, Clone)]
445+
pub struct TimeAndDayCondition {
446+
/// Timezone to use for time comparison
447+
pub timezone: Option<Cow<'static, str>>,
448+
/// Time periods in which the rule should match
449+
pub intervals: Vec<TimeAndDayIntervals>,
450+
}
451+
452+
///
453+
#[derive(Serialize, Deserialize, Debug, Clone)]
454+
pub struct TimeAndDayIntervals {
455+
/// Tuple of hh::mm representing start and end times of the day
456+
pub time_of_day: TimeInterval,
457+
/// 0 = Sunday, 1 = Monday, ..., 7 = Sunday
458+
pub day_of_week: Vec<u32>,
459+
}
460+
461+
#[derive(Serialize, Deserialize, Debug, Clone)]
462+
pub struct TimeInterval {
463+
start_time: Cow<'static, str>,
464+
end_time: Cow<'static, str>,
465+
}
466+
467+
impl TimeInterval {
468+
/// Checks whether the provided time is within the interval
469+
pub fn contains(&self, time: String) -> bool {
470+
// Since MSC specifies ISO 8601 which uses 24h, string comparison is valid.
471+
time >= self.start_time.parse().unwrap() && time <= self.end_time.parse().unwrap()
472+
}
473+
}
474+
441475
/// The collection of push rules for a user.
442476
#[derive(Debug, Clone, Default)]
443477
#[pyclass(frozen)]

0 commit comments

Comments
 (0)