You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

513 lines
17 KiB

  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, BTreeSet};
  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 user mentions that were part of the message.
  61. user_mentions: BTreeSet<String>,
  62. /// True if the message is a room message.
  63. room_mention: bool,
  64. /// The number of users in the room.
  65. room_member_count: u64,
  66. /// The `notifications` section of the current power levels in the room.
  67. notification_power_levels: BTreeMap<String, i64>,
  68. /// The power level of the sender of the event, or None if event is an
  69. /// outlier.
  70. sender_power_level: Option<i64>,
  71. /// The related events, indexed by relation type. Flattened in the same manner as
  72. /// `flattened_keys`.
  73. related_events_flattened: BTreeMap<String, BTreeMap<String, String>>,
  74. /// If msc3664, push rules for related events, is enabled.
  75. related_event_match_enabled: bool,
  76. /// If MSC3931 is applicable, the feature flags for the room version.
  77. room_version_feature_flags: Vec<String>,
  78. /// If MSC3931 (room version feature flags) is enabled. Usually controlled by the same
  79. /// flag as MSC1767 (extensible events core).
  80. msc3931_enabled: bool,
  81. }
  82. #[pymethods]
  83. impl PushRuleEvaluator {
  84. /// Create a new `PushRuleEvaluator`. See struct docstring for details.
  85. #[allow(clippy::too_many_arguments)]
  86. #[new]
  87. pub fn py_new(
  88. flattened_keys: BTreeMap<String, String>,
  89. user_mentions: BTreeSet<String>,
  90. room_mention: bool,
  91. room_member_count: u64,
  92. sender_power_level: Option<i64>,
  93. notification_power_levels: BTreeMap<String, i64>,
  94. related_events_flattened: BTreeMap<String, BTreeMap<String, String>>,
  95. related_event_match_enabled: bool,
  96. room_version_feature_flags: Vec<String>,
  97. msc3931_enabled: bool,
  98. ) -> Result<Self, Error> {
  99. let body = flattened_keys
  100. .get("content.body")
  101. .cloned()
  102. .unwrap_or_default();
  103. Ok(PushRuleEvaluator {
  104. flattened_keys,
  105. body,
  106. user_mentions,
  107. room_mention,
  108. room_member_count,
  109. notification_power_levels,
  110. sender_power_level,
  111. related_events_flattened,
  112. related_event_match_enabled,
  113. room_version_feature_flags,
  114. msc3931_enabled,
  115. })
  116. }
  117. /// Run the evaluator with the given push rules, for the given user ID and
  118. /// display name of the user.
  119. ///
  120. /// Passing in None will skip evaluating rules matching user ID and display
  121. /// name.
  122. ///
  123. /// Returns the set of actions, if any, that match (filtering out any
  124. /// `dont_notify` actions).
  125. pub fn run(
  126. &self,
  127. push_rules: &FilteredPushRules,
  128. user_id: Option<&str>,
  129. display_name: Option<&str>,
  130. ) -> Vec<Action> {
  131. 'outer: for (push_rule, enabled) in push_rules.iter() {
  132. if !enabled {
  133. continue;
  134. }
  135. let rule_id = &push_rule.rule_id().to_string();
  136. let extev_flag = &RoomVersionFeatures::ExtensibleEvents.as_str().to_string();
  137. let supports_extensible_events = self.room_version_feature_flags.contains(extev_flag);
  138. let safe_from_rver_condition = SAFE_EXTENSIBLE_EVENTS_RULE_IDS.contains(rule_id);
  139. let mut has_rver_condition = false;
  140. for condition in push_rule.conditions.iter() {
  141. has_rver_condition |= matches!(
  142. condition,
  143. // per MSC3932, we just need *any* room version condition to match
  144. Condition::Known(KnownCondition::RoomVersionSupports { feature: _ }),
  145. );
  146. match self.match_condition(condition, user_id, display_name) {
  147. Ok(true) => {}
  148. Ok(false) => continue 'outer,
  149. Err(err) => {
  150. warn!("Condition match failed {err}");
  151. continue 'outer;
  152. }
  153. }
  154. }
  155. // MSC3932: Disable push rules in extensible event-supporting room versions if they
  156. // don't describe *any* MSC3931 room version condition, unless the rule is on the
  157. // safe list.
  158. if !has_rver_condition && !safe_from_rver_condition && supports_extensible_events {
  159. continue;
  160. }
  161. let actions = push_rule
  162. .actions
  163. .iter()
  164. // Filter out "dont_notify" actions, as we don't store them.
  165. .filter(|a| **a != Action::DontNotify)
  166. .cloned()
  167. .collect();
  168. return actions;
  169. }
  170. Vec::new()
  171. }
  172. /// Check if the given condition matches.
  173. fn matches(
  174. &self,
  175. condition: Condition,
  176. user_id: Option<&str>,
  177. display_name: Option<&str>,
  178. ) -> bool {
  179. match self.match_condition(&condition, user_id, display_name) {
  180. Ok(true) => true,
  181. Ok(false) => false,
  182. Err(err) => {
  183. warn!("Condition match failed {err}");
  184. false
  185. }
  186. }
  187. }
  188. }
  189. impl PushRuleEvaluator {
  190. /// Match a given `Condition` for a push rule.
  191. pub fn match_condition(
  192. &self,
  193. condition: &Condition,
  194. user_id: Option<&str>,
  195. display_name: Option<&str>,
  196. ) -> Result<bool, Error> {
  197. let known_condition = match condition {
  198. Condition::Known(known) => known,
  199. Condition::Unknown(_) => {
  200. return Ok(false);
  201. }
  202. };
  203. let result = match known_condition {
  204. KnownCondition::EventMatch(event_match) => {
  205. self.match_event_match(event_match, user_id)?
  206. }
  207. KnownCondition::RelatedEventMatch(event_match) => {
  208. self.match_related_event_match(event_match, user_id)?
  209. }
  210. KnownCondition::IsUserMention => {
  211. if let Some(uid) = user_id {
  212. self.user_mentions.contains(uid)
  213. } else {
  214. false
  215. }
  216. }
  217. KnownCondition::IsRoomMention => self.room_mention,
  218. KnownCondition::ContainsDisplayName => {
  219. if let Some(dn) = display_name {
  220. if !dn.is_empty() {
  221. get_glob_matcher(dn, GlobMatchType::Word)?.is_match(&self.body)?
  222. } else {
  223. // We specifically ignore empty display names, as otherwise
  224. // they would always match.
  225. false
  226. }
  227. } else {
  228. false
  229. }
  230. }
  231. KnownCondition::RoomMemberCount { is } => {
  232. if let Some(is) = is {
  233. self.match_member_count(is)?
  234. } else {
  235. false
  236. }
  237. }
  238. KnownCondition::SenderNotificationPermission { key } => {
  239. if let Some(sender_power_level) = &self.sender_power_level {
  240. let required_level = self
  241. .notification_power_levels
  242. .get(key.as_ref())
  243. .copied()
  244. .unwrap_or(50);
  245. *sender_power_level >= required_level
  246. } else {
  247. false
  248. }
  249. }
  250. KnownCondition::RoomVersionSupports { feature } => {
  251. if !self.msc3931_enabled {
  252. false
  253. } else {
  254. let flag = feature.to_string();
  255. KNOWN_RVER_FLAGS.contains(&flag)
  256. && self.room_version_feature_flags.contains(&flag)
  257. }
  258. }
  259. };
  260. Ok(result)
  261. }
  262. /// Evaluates a `event_match` condition.
  263. fn match_event_match(
  264. &self,
  265. event_match: &EventMatchCondition,
  266. user_id: Option<&str>,
  267. ) -> Result<bool, Error> {
  268. let pattern = if let Some(pattern) = &event_match.pattern {
  269. pattern
  270. } else if let Some(pattern_type) = &event_match.pattern_type {
  271. // The `pattern_type` can either be "user_id" or "user_localpart",
  272. // either way if we don't have a `user_id` then the condition can't
  273. // match.
  274. let user_id = if let Some(user_id) = user_id {
  275. user_id
  276. } else {
  277. return Ok(false);
  278. };
  279. match &**pattern_type {
  280. "user_id" => user_id,
  281. "user_localpart" => get_localpart_from_id(user_id)?,
  282. _ => return Ok(false),
  283. }
  284. } else {
  285. return Ok(false);
  286. };
  287. let haystack = if let Some(haystack) = self.flattened_keys.get(&*event_match.key) {
  288. haystack
  289. } else {
  290. return Ok(false);
  291. };
  292. // For the content.body we match against "words", but for everything
  293. // else we match against the entire value.
  294. let match_type = if event_match.key == "content.body" {
  295. GlobMatchType::Word
  296. } else {
  297. GlobMatchType::Whole
  298. };
  299. let mut compiled_pattern = get_glob_matcher(pattern, match_type)?;
  300. compiled_pattern.is_match(haystack)
  301. }
  302. /// Evaluates a `related_event_match` condition. (MSC3664)
  303. fn match_related_event_match(
  304. &self,
  305. event_match: &RelatedEventMatchCondition,
  306. user_id: Option<&str>,
  307. ) -> Result<bool, Error> {
  308. // First check if related event matching is enabled...
  309. if !self.related_event_match_enabled {
  310. return Ok(false);
  311. }
  312. // get the related event, fail if there is none.
  313. let event = if let Some(event) = self.related_events_flattened.get(&*event_match.rel_type) {
  314. event
  315. } else {
  316. return Ok(false);
  317. };
  318. // If we are not matching fallbacks, don't match if our special key indicating this is a
  319. // fallback relation is not present.
  320. if !event_match.include_fallbacks.unwrap_or(false)
  321. && event.contains_key("im.vector.is_falling_back")
  322. {
  323. return Ok(false);
  324. }
  325. // if we have no key, accept the event as matching, if it existed without matching any
  326. // fields.
  327. let key = if let Some(key) = &event_match.key {
  328. key
  329. } else {
  330. return Ok(true);
  331. };
  332. let pattern = if let Some(pattern) = &event_match.pattern {
  333. pattern
  334. } else if let Some(pattern_type) = &event_match.pattern_type {
  335. // The `pattern_type` can either be "user_id" or "user_localpart",
  336. // either way if we don't have a `user_id` then the condition can't
  337. // match.
  338. let user_id = if let Some(user_id) = user_id {
  339. user_id
  340. } else {
  341. return Ok(false);
  342. };
  343. match &**pattern_type {
  344. "user_id" => user_id,
  345. "user_localpart" => get_localpart_from_id(user_id)?,
  346. _ => return Ok(false),
  347. }
  348. } else {
  349. return Ok(false);
  350. };
  351. let haystack = if let Some(haystack) = event.get(&**key) {
  352. haystack
  353. } else {
  354. return Ok(false);
  355. };
  356. // For the content.body we match against "words", but for everything
  357. // else we match against the entire value.
  358. let match_type = if key == "content.body" {
  359. GlobMatchType::Word
  360. } else {
  361. GlobMatchType::Whole
  362. };
  363. let mut compiled_pattern = get_glob_matcher(pattern, match_type)?;
  364. compiled_pattern.is_match(haystack)
  365. }
  366. /// Match the member count against an 'is' condition
  367. /// The `is` condition can be things like '>2', '==3' or even just '4'.
  368. fn match_member_count(&self, is: &str) -> Result<bool, Error> {
  369. let captures = INEQUALITY_EXPR.captures(is).context("bad 'is' clause")?;
  370. let ineq = captures.get(1).map_or("==", |m| m.as_str());
  371. let rhs: u64 = captures
  372. .get(2)
  373. .context("missing number")?
  374. .as_str()
  375. .parse()?;
  376. let matches = match ineq {
  377. "" | "==" => self.room_member_count == rhs,
  378. "<" => self.room_member_count < rhs,
  379. ">" => self.room_member_count > rhs,
  380. ">=" => self.room_member_count >= rhs,
  381. "<=" => self.room_member_count <= rhs,
  382. _ => false,
  383. };
  384. Ok(matches)
  385. }
  386. }
  387. #[test]
  388. fn push_rule_evaluator() {
  389. let mut flattened_keys = BTreeMap::new();
  390. flattened_keys.insert("content.body".to_string(), "foo bar bob hello".to_string());
  391. let evaluator = PushRuleEvaluator::py_new(
  392. flattened_keys,
  393. BTreeSet::new(),
  394. false,
  395. 10,
  396. Some(0),
  397. BTreeMap::new(),
  398. BTreeMap::new(),
  399. true,
  400. vec![],
  401. true,
  402. )
  403. .unwrap();
  404. let result = evaluator.run(&FilteredPushRules::default(), None, Some("bob"));
  405. assert_eq!(result.len(), 3);
  406. }
  407. #[test]
  408. fn test_requires_room_version_supports_condition() {
  409. use std::borrow::Cow;
  410. use crate::push::{PushRule, PushRules};
  411. let mut flattened_keys = BTreeMap::new();
  412. flattened_keys.insert("content.body".to_string(), "foo bar bob hello".to_string());
  413. let flags = vec![RoomVersionFeatures::ExtensibleEvents.as_str().to_string()];
  414. let evaluator = PushRuleEvaluator::py_new(
  415. flattened_keys,
  416. BTreeSet::new(),
  417. false,
  418. 10,
  419. Some(0),
  420. BTreeMap::new(),
  421. BTreeMap::new(),
  422. false,
  423. flags,
  424. true,
  425. )
  426. .unwrap();
  427. // first test: are the master and contains_user_name rules excluded from the "requires room
  428. // version condition" check?
  429. let mut result = evaluator.run(
  430. &FilteredPushRules::default(),
  431. Some("@bob:example.org"),
  432. None,
  433. );
  434. assert_eq!(result.len(), 3);
  435. // second test: if an appropriate push rule is in play, does it get handled?
  436. let custom_rule = PushRule {
  437. rule_id: Cow::from("global/underride/.org.example.extensible"),
  438. priority_class: 1, // underride
  439. conditions: Cow::from(vec![Condition::Known(
  440. KnownCondition::RoomVersionSupports {
  441. feature: Cow::from(RoomVersionFeatures::ExtensibleEvents.as_str().to_string()),
  442. },
  443. )]),
  444. actions: Cow::from(vec![Action::Notify]),
  445. default: false,
  446. default_enabled: true,
  447. };
  448. let rules = PushRules::new(vec![custom_rule]);
  449. result = evaluator.run(
  450. &FilteredPushRules::py_new(rules, BTreeMap::new(), true, false, true, false),
  451. None,
  452. None,
  453. );
  454. assert_eq!(result.len(), 1);
  455. }