evaluator.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. // Copyright 2022 The Matrix.org Foundation C.I.C.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. use std::collections::BTreeMap;
  15. use anyhow::{Context, Error};
  16. use lazy_static::lazy_static;
  17. use log::warn;
  18. use pyo3::prelude::*;
  19. use regex::Regex;
  20. use super::{
  21. utils::{get_glob_matcher, get_localpart_from_id, GlobMatchType},
  22. Action, Condition, EventMatchCondition, FilteredPushRules, KnownCondition,
  23. RelatedEventMatchCondition,
  24. };
  25. lazy_static! {
  26. /// Used to parse the `is` clause in the room member count condition.
  27. static ref INEQUALITY_EXPR: Regex = Regex::new(r"^([=<>]*)([0-9]+)$").expect("valid regex");
  28. /// Used to determine which MSC3931 room version feature flags are actually known to
  29. /// the push evaluator.
  30. static ref KNOWN_RVER_FLAGS: Vec<String> = vec![
  31. RoomVersionFeatures::ExtensibleEvents.as_str().to_string(),
  32. ];
  33. /// The "safe" rule IDs which are not affected by MSC3932's behaviour (room versions which
  34. /// declare Extensible Events support ultimately *disable* push rules which do not declare
  35. /// *any* MSC3931 room_version_supports condition).
  36. static ref SAFE_EXTENSIBLE_EVENTS_RULE_IDS: Vec<String> = vec![
  37. "global/override/.m.rule.master".to_string(),
  38. "global/override/.m.rule.roomnotif".to_string(),
  39. "global/content/.m.rule.contains_user_name".to_string(),
  40. ];
  41. }
  42. enum RoomVersionFeatures {
  43. ExtensibleEvents,
  44. }
  45. impl RoomVersionFeatures {
  46. fn as_str(&self) -> &'static str {
  47. match self {
  48. RoomVersionFeatures::ExtensibleEvents => "org.matrix.msc3932.extensible_events",
  49. }
  50. }
  51. }
  52. /// Allows running a set of push rules against a particular event.
  53. #[pyclass]
  54. pub struct PushRuleEvaluator {
  55. /// A mapping of "flattened" keys to string values in the event, e.g.
  56. /// includes things like "type" and "content.msgtype".
  57. flattened_keys: BTreeMap<String, String>,
  58. /// The "content.body", if any.
  59. body: String,
  60. /// The number of users in the room.
  61. room_member_count: u64,
  62. /// The `notifications` section of the current power levels in the room.
  63. notification_power_levels: BTreeMap<String, i64>,
  64. /// The power level of the sender of the event, or None if event is an
  65. /// outlier.
  66. sender_power_level: Option<i64>,
  67. /// The related events, indexed by relation type. Flattened in the same manner as
  68. /// `flattened_keys`.
  69. related_events_flattened: BTreeMap<String, BTreeMap<String, String>>,
  70. /// If msc3664, push rules for related events, is enabled.
  71. related_event_match_enabled: bool,
  72. /// If MSC3931 is applicable, the feature flags for the room version.
  73. room_version_feature_flags: Vec<String>,
  74. /// If MSC3931 (room version feature flags) is enabled. Usually controlled by the same
  75. /// flag as MSC1767 (extensible events core).
  76. msc3931_enabled: bool,
  77. }
  78. #[pymethods]
  79. impl PushRuleEvaluator {
  80. /// Create a new `PushRuleEvaluator`. See struct docstring for details.
  81. #[allow(clippy::too_many_arguments)]
  82. #[new]
  83. pub fn py_new(
  84. flattened_keys: BTreeMap<String, String>,
  85. room_member_count: u64,
  86. sender_power_level: Option<i64>,
  87. notification_power_levels: BTreeMap<String, i64>,
  88. related_events_flattened: BTreeMap<String, BTreeMap<String, String>>,
  89. related_event_match_enabled: bool,
  90. room_version_feature_flags: Vec<String>,
  91. msc3931_enabled: bool,
  92. ) -> Result<Self, Error> {
  93. let body = flattened_keys
  94. .get("content.body")
  95. .cloned()
  96. .unwrap_or_default();
  97. Ok(PushRuleEvaluator {
  98. flattened_keys,
  99. body,
  100. room_member_count,
  101. notification_power_levels,
  102. sender_power_level,
  103. related_events_flattened,
  104. related_event_match_enabled,
  105. room_version_feature_flags,
  106. msc3931_enabled,
  107. })
  108. }
  109. /// Run the evaluator with the given push rules, for the given user ID and
  110. /// display name of the user.
  111. ///
  112. /// Passing in None will skip evaluating rules matching user ID and display
  113. /// name.
  114. ///
  115. /// Returns the set of actions, if any, that match (filtering out any
  116. /// `dont_notify` actions).
  117. pub fn run(
  118. &self,
  119. push_rules: &FilteredPushRules,
  120. user_id: Option<&str>,
  121. display_name: Option<&str>,
  122. ) -> Vec<Action> {
  123. 'outer: for (push_rule, enabled) in push_rules.iter() {
  124. if !enabled {
  125. continue;
  126. }
  127. let rule_id = &push_rule.rule_id().to_string();
  128. let extev_flag = &RoomVersionFeatures::ExtensibleEvents.as_str().to_string();
  129. let supports_extensible_events = self.room_version_feature_flags.contains(extev_flag);
  130. let safe_from_rver_condition = SAFE_EXTENSIBLE_EVENTS_RULE_IDS.contains(rule_id);
  131. let mut has_rver_condition = false;
  132. for condition in push_rule.conditions.iter() {
  133. has_rver_condition |= matches!(
  134. condition,
  135. // per MSC3932, we just need *any* room version condition to match
  136. Condition::Known(KnownCondition::RoomVersionSupports { feature: _ }),
  137. );
  138. match self.match_condition(condition, user_id, display_name) {
  139. Ok(true) => {}
  140. Ok(false) => continue 'outer,
  141. Err(err) => {
  142. warn!("Condition match failed {err}");
  143. continue 'outer;
  144. }
  145. }
  146. }
  147. // MSC3932: Disable push rules in extensible event-supporting room versions if they
  148. // don't describe *any* MSC3931 room version condition, unless the rule is on the
  149. // safe list.
  150. if !has_rver_condition && !safe_from_rver_condition && supports_extensible_events {
  151. continue;
  152. }
  153. let actions = push_rule
  154. .actions
  155. .iter()
  156. // Filter out "dont_notify" actions, as we don't store them.
  157. .filter(|a| **a != Action::DontNotify)
  158. .cloned()
  159. .collect();
  160. return actions;
  161. }
  162. Vec::new()
  163. }
  164. /// Check if the given condition matches.
  165. fn matches(
  166. &self,
  167. condition: Condition,
  168. user_id: Option<&str>,
  169. display_name: Option<&str>,
  170. ) -> bool {
  171. match self.match_condition(&condition, user_id, display_name) {
  172. Ok(true) => true,
  173. Ok(false) => false,
  174. Err(err) => {
  175. warn!("Condition match failed {err}");
  176. false
  177. }
  178. }
  179. }
  180. }
  181. impl PushRuleEvaluator {
  182. /// Match a given `Condition` for a push rule.
  183. pub fn match_condition(
  184. &self,
  185. condition: &Condition,
  186. user_id: Option<&str>,
  187. display_name: Option<&str>,
  188. ) -> Result<bool, Error> {
  189. let known_condition = match condition {
  190. Condition::Known(known) => known,
  191. Condition::Unknown(_) => {
  192. return Ok(false);
  193. }
  194. };
  195. let result = match known_condition {
  196. KnownCondition::EventMatch(event_match) => {
  197. self.match_event_match(event_match, user_id)?
  198. }
  199. KnownCondition::RelatedEventMatch(event_match) => {
  200. self.match_related_event_match(event_match, user_id)?
  201. }
  202. KnownCondition::ContainsDisplayName => {
  203. if let Some(dn) = display_name {
  204. if !dn.is_empty() {
  205. get_glob_matcher(dn, GlobMatchType::Word)?.is_match(&self.body)?
  206. } else {
  207. // We specifically ignore empty display names, as otherwise
  208. // they would always match.
  209. false
  210. }
  211. } else {
  212. false
  213. }
  214. }
  215. KnownCondition::RoomMemberCount { is } => {
  216. if let Some(is) = is {
  217. self.match_member_count(is)?
  218. } else {
  219. false
  220. }
  221. }
  222. KnownCondition::SenderNotificationPermission { key } => {
  223. if let Some(sender_power_level) = &self.sender_power_level {
  224. let required_level = self
  225. .notification_power_levels
  226. .get(key.as_ref())
  227. .copied()
  228. .unwrap_or(50);
  229. *sender_power_level >= required_level
  230. } else {
  231. false
  232. }
  233. }
  234. KnownCondition::RoomVersionSupports { feature } => {
  235. if !self.msc3931_enabled {
  236. false
  237. } else {
  238. let flag = feature.to_string();
  239. KNOWN_RVER_FLAGS.contains(&flag)
  240. && self.room_version_feature_flags.contains(&flag)
  241. }
  242. }
  243. };
  244. Ok(result)
  245. }
  246. /// Evaluates a `event_match` condition.
  247. fn match_event_match(
  248. &self,
  249. event_match: &EventMatchCondition,
  250. user_id: Option<&str>,
  251. ) -> Result<bool, Error> {
  252. let pattern = if let Some(pattern) = &event_match.pattern {
  253. pattern
  254. } else if let Some(pattern_type) = &event_match.pattern_type {
  255. // The `pattern_type` can either be "user_id" or "user_localpart",
  256. // either way if we don't have a `user_id` then the condition can't
  257. // match.
  258. let user_id = if let Some(user_id) = user_id {
  259. user_id
  260. } else {
  261. return Ok(false);
  262. };
  263. match &**pattern_type {
  264. "user_id" => user_id,
  265. "user_localpart" => get_localpart_from_id(user_id)?,
  266. _ => return Ok(false),
  267. }
  268. } else {
  269. return Ok(false);
  270. };
  271. let haystack = if let Some(haystack) = self.flattened_keys.get(&*event_match.key) {
  272. haystack
  273. } else {
  274. return Ok(false);
  275. };
  276. // For the content.body we match against "words", but for everything
  277. // else we match against the entire value.
  278. let match_type = if event_match.key == "content.body" {
  279. GlobMatchType::Word
  280. } else {
  281. GlobMatchType::Whole
  282. };
  283. let mut compiled_pattern = get_glob_matcher(pattern, match_type)?;
  284. compiled_pattern.is_match(haystack)
  285. }
  286. /// Evaluates a `related_event_match` condition. (MSC3664)
  287. fn match_related_event_match(
  288. &self,
  289. event_match: &RelatedEventMatchCondition,
  290. user_id: Option<&str>,
  291. ) -> Result<bool, Error> {
  292. // First check if related event matching is enabled...
  293. if !self.related_event_match_enabled {
  294. return Ok(false);
  295. }
  296. // get the related event, fail if there is none.
  297. let event = if let Some(event) = self.related_events_flattened.get(&*event_match.rel_type) {
  298. event
  299. } else {
  300. return Ok(false);
  301. };
  302. // If we are not matching fallbacks, don't match if our special key indicating this is a
  303. // fallback relation is not present.
  304. if !event_match.include_fallbacks.unwrap_or(false)
  305. && event.contains_key("im.vector.is_falling_back")
  306. {
  307. return Ok(false);
  308. }
  309. // if we have no key, accept the event as matching, if it existed without matching any
  310. // fields.
  311. let key = if let Some(key) = &event_match.key {
  312. key
  313. } else {
  314. return Ok(true);
  315. };
  316. let pattern = if let Some(pattern) = &event_match.pattern {
  317. pattern
  318. } else if let Some(pattern_type) = &event_match.pattern_type {
  319. // The `pattern_type` can either be "user_id" or "user_localpart",
  320. // either way if we don't have a `user_id` then the condition can't
  321. // match.
  322. let user_id = if let Some(user_id) = user_id {
  323. user_id
  324. } else {
  325. return Ok(false);
  326. };
  327. match &**pattern_type {
  328. "user_id" => user_id,
  329. "user_localpart" => get_localpart_from_id(user_id)?,
  330. _ => return Ok(false),
  331. }
  332. } else {
  333. return Ok(false);
  334. };
  335. let haystack = if let Some(haystack) = event.get(&**key) {
  336. haystack
  337. } else {
  338. return Ok(false);
  339. };
  340. // For the content.body we match against "words", but for everything
  341. // else we match against the entire value.
  342. let match_type = if key == "content.body" {
  343. GlobMatchType::Word
  344. } else {
  345. GlobMatchType::Whole
  346. };
  347. let mut compiled_pattern = get_glob_matcher(pattern, match_type)?;
  348. compiled_pattern.is_match(haystack)
  349. }
  350. /// Match the member count against an 'is' condition
  351. /// The `is` condition can be things like '>2', '==3' or even just '4'.
  352. fn match_member_count(&self, is: &str) -> Result<bool, Error> {
  353. let captures = INEQUALITY_EXPR.captures(is).context("bad 'is' clause")?;
  354. let ineq = captures.get(1).map_or("==", |m| m.as_str());
  355. let rhs: u64 = captures
  356. .get(2)
  357. .context("missing number")?
  358. .as_str()
  359. .parse()?;
  360. let matches = match ineq {
  361. "" | "==" => self.room_member_count == rhs,
  362. "<" => self.room_member_count < rhs,
  363. ">" => self.room_member_count > rhs,
  364. ">=" => self.room_member_count >= rhs,
  365. "<=" => self.room_member_count <= rhs,
  366. _ => false,
  367. };
  368. Ok(matches)
  369. }
  370. }
  371. #[test]
  372. fn push_rule_evaluator() {
  373. let mut flattened_keys = BTreeMap::new();
  374. flattened_keys.insert("content.body".to_string(), "foo bar bob hello".to_string());
  375. let evaluator = PushRuleEvaluator::py_new(
  376. flattened_keys,
  377. 10,
  378. Some(0),
  379. BTreeMap::new(),
  380. BTreeMap::new(),
  381. true,
  382. vec![],
  383. true,
  384. )
  385. .unwrap();
  386. let result = evaluator.run(&FilteredPushRules::default(), None, Some("bob"));
  387. assert_eq!(result.len(), 3);
  388. }
  389. #[test]
  390. fn test_requires_room_version_supports_condition() {
  391. use std::borrow::Cow;
  392. use crate::push::{PushRule, PushRules};
  393. let mut flattened_keys = BTreeMap::new();
  394. flattened_keys.insert("content.body".to_string(), "foo bar bob hello".to_string());
  395. let flags = vec![RoomVersionFeatures::ExtensibleEvents.as_str().to_string()];
  396. let evaluator = PushRuleEvaluator::py_new(
  397. flattened_keys,
  398. 10,
  399. Some(0),
  400. BTreeMap::new(),
  401. BTreeMap::new(),
  402. false,
  403. flags,
  404. true,
  405. )
  406. .unwrap();
  407. // first test: are the master and contains_user_name rules excluded from the "requires room
  408. // version condition" check?
  409. let mut result = evaluator.run(
  410. &FilteredPushRules::default(),
  411. Some("@bob:example.org"),
  412. None,
  413. );
  414. assert_eq!(result.len(), 3);
  415. // second test: if an appropriate push rule is in play, does it get handled?
  416. let custom_rule = PushRule {
  417. rule_id: Cow::from("global/underride/.org.example.extensible"),
  418. priority_class: 1, // underride
  419. conditions: Cow::from(vec![Condition::Known(
  420. KnownCondition::RoomVersionSupports {
  421. feature: Cow::from(RoomVersionFeatures::ExtensibleEvents.as_str().to_string()),
  422. },
  423. )]),
  424. actions: Cow::from(vec![Action::Notify]),
  425. default: false,
  426. default_enabled: true,
  427. };
  428. let rules = PushRules::new(vec![custom_rule]);
  429. result = evaluator.run(
  430. &FilteredPushRules::py_new(rules, BTreeMap::new(), true, true),
  431. None,
  432. None,
  433. );
  434. assert_eq!(result.len(), 1);
  435. }