Skip to content

Commit

Permalink
feat: time and day push rule condition rust
Browse files Browse the repository at this point in the history
  • Loading branch information
hanadi92 committed Jan 27, 2024
1 parent 232adfb commit 2b70923
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 1 deletion.
1 change: 1 addition & 0 deletions changelog.d/16858.feature
Original file line number Diff line number Diff line change
@@ -0,0 +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.
1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pythonize = "0.20.0"
regex = "1.6.0"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.85"
chrono = "0.4.33"

[features]
extension-module = ["pyo3/extension-module"]
Expand Down
112 changes: 111 additions & 1 deletion rust/src/push/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use std::borrow::Cow;
use std::collections::BTreeMap;

use anyhow::{Context, Error};
use chrono::Datelike;
use lazy_static::lazy_static;
use log::warn;
use pyo3::prelude::*;
Expand All @@ -31,7 +32,7 @@ use regex::Regex;
use super::{
utils::{get_glob_matcher, get_localpart_from_id, GlobMatchType},
Action, Condition, EventPropertyIsCondition, FilteredPushRules, KnownCondition,
SimpleJsonValue,
SimpleJsonValue, TimeAndDayIntervals,
};
use crate::push::{EventMatchPatternType, JsonValue};

Expand Down Expand Up @@ -105,6 +106,9 @@ pub struct PushRuleEvaluator {
/// If MSC3931 (room version feature flags) is enabled. Usually controlled by the same
/// flag as MSC1767 (extensible events core).
msc3931_enabled: bool,

/// If MSC3767 (time based notification filtering push rule condition) is enabled
msc3767_time_and_day: bool,
}

#[pymethods]
Expand All @@ -122,6 +126,7 @@ impl PushRuleEvaluator {
related_event_match_enabled,
room_version_feature_flags,
msc3931_enabled,
msc3767_time_and_day,
))]
pub fn py_new(
flattened_keys: BTreeMap<String, JsonValue>,
Expand All @@ -133,6 +138,7 @@ impl PushRuleEvaluator {
related_event_match_enabled: bool,
room_version_feature_flags: Vec<String>,
msc3931_enabled: bool,
msc3767_time_and_day: bool,
) -> Result<Self, Error> {
let body = match flattened_keys.get("content.body") {
Some(JsonValue::Value(SimpleJsonValue::Str(s))) => s.clone().into_owned(),
Expand All @@ -150,6 +156,7 @@ impl PushRuleEvaluator {
related_event_match_enabled,
room_version_feature_flags,
msc3931_enabled,
msc3767_time_and_day,
})
}

Expand Down Expand Up @@ -384,6 +391,13 @@ impl PushRuleEvaluator {
&& self.room_version_feature_flags.contains(&flag)
}
}
KnownCondition::TimeAndDay(time_and_day) => {
if !self.msc3767_time_and_day {
false
} else {
self.match_time_and_day(time_and_day.timezone.clone(), &time_and_day.intervals)?
}
}
};

Ok(result)
Expand Down Expand Up @@ -507,6 +521,31 @@ impl PushRuleEvaluator {

Ok(matches)
}

///
fn match_time_and_day(
&self,
_timezone: Option<Cow<str>>,
intervals: &[TimeAndDayIntervals],
) -> Result<bool, Error> {
// Temp Notes from spec:
// The timezone to use for time comparison. This format allows for automatic DST handling.
// Intervals representing time periods in which the rule should match. Evaluated with an OR condition.
//
// time_of_day condition is met when the server's timezone-adjusted time is between the values of the tuple,
// or when no time_of_day is set on the interval. Values are inclusive.
// day_of_week condition is met when the server's timezone-adjusted day is included in the array.
// next step -> consider timezone if given
let now = chrono::Utc::now();
let today = now.weekday().num_days_from_sunday();
let current_time = now.time().format("%H:%M").to_string();
let matches = intervals.iter().any(|interval| {
interval.day_of_week.contains(&today)
&& interval.time_of_day.contains(current_time.to_string())
});

Ok(matches)
}
}

#[test]
Expand All @@ -526,6 +565,7 @@ fn push_rule_evaluator() {
true,
vec![],
true,
true,
)
.unwrap();

Expand Down Expand Up @@ -555,6 +595,7 @@ fn test_requires_room_version_supports_condition() {
false,
flags,
true,
true,
)
.unwrap();

Expand Down Expand Up @@ -588,3 +629,72 @@ fn test_requires_room_version_supports_condition() {
);
assert_eq!(result.len(), 1);
}

#[test]
fn test_time_and_day_condition() {
use chrono::Duration;

use crate::push::{PushRule, PushRules, TimeAndDayCondition, TimeInterval};

let mut flattened_keys = BTreeMap::new();
flattened_keys.insert(
"content.body".to_string(),
JsonValue::Value(SimpleJsonValue::Str(Cow::Borrowed("foo bar bob hello"))),
);
let evaluator = PushRuleEvaluator::py_new(
flattened_keys,
false,
10,
Some(0),
BTreeMap::new(),
BTreeMap::new(),
true,
vec![],
true,
true,
)
.unwrap();

// smoke test: notify is working with other conditions
let result = evaluator.run(
&FilteredPushRules::default(),
Some("@bob:example.org"),
None,
);
assert_eq!(result.len(), 3);

// for testing sakes, use current duration of two hours behind and forward of now as dnd
let test_time = chrono::Utc::now();
let test_start_time = (test_time - Duration::hours(2)).format("%H:%M").to_string();
let test_end_time = (test_time + Duration::hours(2)).format("%H:%M").to_string();

// time and day condition in push rule
let custom_rule = PushRule {
rule_id: Cow::from(".m.rule.master"),
priority_class: 5, // override
conditions: Cow::from(vec![Condition::Known(KnownCondition::TimeAndDay(
TimeAndDayCondition {
timezone: None,
intervals: vec![TimeAndDayIntervals {
time_of_day: TimeInterval {
start_time: Cow::from(test_start_time),
end_time: Cow::from(test_end_time),
},
day_of_week: vec![6],
}],
},
))]),
actions: Cow::from(vec![Action::DontNotify]),
default: true,
default_enabled: true,
};
let rules = PushRules::new(vec![custom_rule]);
let result = evaluator.run(
&FilteredPushRules::py_new(rules, BTreeMap::new(), true, false, true, false),
None,
None,
);

// dnd time, dont_notify
assert_eq!(result.len(), 0);
}
34 changes: 34 additions & 0 deletions rust/src/push/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,8 @@ pub enum KnownCondition {
RoomVersionSupports {
feature: Cow<'static, str>,
},
#[serde(rename = "org.matrix.msc3767.time_and_day")]
TimeAndDay(TimeAndDayCondition),
}

impl IntoPy<PyObject> for Condition {
Expand Down Expand Up @@ -438,6 +440,38 @@ pub struct RelatedEventMatchTypeCondition {
pub include_fallbacks: Option<bool>,
}

/// The body of [`KnownCondition::TimeAndDay`]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TimeAndDayCondition {
/// Timezone to use for time comparison
pub timezone: Option<Cow<'static, str>>,
/// Time periods in which the rule should match
pub intervals: Vec<TimeAndDayIntervals>,
}

///
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TimeAndDayIntervals {
/// Tuple of hh::mm representing start and end times of the day
pub time_of_day: TimeInterval,
/// 0 = Sunday, 1 = Monday, ..., 7 = Sunday
pub day_of_week: Vec<u32>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TimeInterval {
start_time: Cow<'static, str>,
end_time: Cow<'static, str>,
}

impl TimeInterval {
/// Checks whether the provided time is within the interval
pub fn contains(&self, time: String) -> bool {
// Since MSC specifies ISO 8601 which uses 24h, string comparison is valid.
time >= self.start_time.parse().unwrap() && time <= self.end_time.parse().unwrap()
}
}

/// The collection of push rules for a user.
#[derive(Debug, Clone, Default)]
#[pyclass(frozen)]
Expand Down

0 comments on commit 2b70923

Please sign in to comment.