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.
 
 
 
 
 
 

375 lines
12 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::{
  15. borrow::Cow,
  16. collections::{BTreeMap, BTreeSet},
  17. };
  18. use anyhow::{Context, Error};
  19. use lazy_static::lazy_static;
  20. use log::warn;
  21. use pyo3::prelude::*;
  22. use regex::Regex;
  23. use super::{
  24. utils::{get_glob_matcher, get_localpart_from_id, GlobMatchType},
  25. Action, Condition, EventMatchCondition, FilteredPushRules, KnownCondition,
  26. };
  27. lazy_static! {
  28. /// Used to parse the `is` clause in the room member count condition.
  29. static ref INEQUALITY_EXPR: Regex = Regex::new(r"^([=<>]*)([0-9]+)$").expect("valid regex");
  30. }
  31. /// Allows running a set of push rules against a particular event.
  32. #[pyclass]
  33. pub struct PushRuleEvaluator {
  34. /// A mapping of "flattened" keys to string values in the event, e.g.
  35. /// includes things like "type" and "content.msgtype".
  36. flattened_keys: BTreeMap<String, String>,
  37. /// The "content.body", if any.
  38. body: String,
  39. /// The number of users in the room.
  40. room_member_count: u64,
  41. /// The `notifications` section of the current power levels in the room.
  42. notification_power_levels: BTreeMap<String, i64>,
  43. /// The relations related to the event as a mapping from relation type to
  44. /// set of sender/event type 2-tuples.
  45. relations: BTreeMap<String, BTreeSet<(String, String)>>,
  46. /// Is running "relation" conditions enabled?
  47. relation_match_enabled: bool,
  48. /// The power level of the sender of the event, or None if event is an
  49. /// outlier.
  50. sender_power_level: Option<i64>,
  51. }
  52. #[pymethods]
  53. impl PushRuleEvaluator {
  54. /// Create a new `PushRuleEvaluator`. See struct docstring for details.
  55. #[new]
  56. pub fn py_new(
  57. flattened_keys: BTreeMap<String, String>,
  58. room_member_count: u64,
  59. sender_power_level: Option<i64>,
  60. notification_power_levels: BTreeMap<String, i64>,
  61. relations: BTreeMap<String, BTreeSet<(String, String)>>,
  62. relation_match_enabled: bool,
  63. ) -> Result<Self, Error> {
  64. let body = flattened_keys
  65. .get("content.body")
  66. .cloned()
  67. .unwrap_or_default();
  68. Ok(PushRuleEvaluator {
  69. flattened_keys,
  70. body,
  71. room_member_count,
  72. notification_power_levels,
  73. relations,
  74. relation_match_enabled,
  75. sender_power_level,
  76. })
  77. }
  78. /// Run the evaluator with the given push rules, for the given user ID and
  79. /// display name of the user.
  80. ///
  81. /// Passing in None will skip evaluating rules matching user ID and display
  82. /// name.
  83. ///
  84. /// Returns the set of actions, if any, that match (filtering out any
  85. /// `dont_notify` actions).
  86. pub fn run(
  87. &self,
  88. push_rules: &FilteredPushRules,
  89. user_id: Option<&str>,
  90. display_name: Option<&str>,
  91. ) -> Vec<Action> {
  92. 'outer: for (push_rule, enabled) in push_rules.iter() {
  93. if !enabled {
  94. continue;
  95. }
  96. for condition in push_rule.conditions.iter() {
  97. match self.match_condition(condition, user_id, display_name) {
  98. Ok(true) => {}
  99. Ok(false) => continue 'outer,
  100. Err(err) => {
  101. warn!("Condition match failed {err}");
  102. continue 'outer;
  103. }
  104. }
  105. }
  106. let actions = push_rule
  107. .actions
  108. .iter()
  109. // Filter out "dont_notify" actions, as we don't store them.
  110. .filter(|a| **a != Action::DontNotify)
  111. .cloned()
  112. .collect();
  113. return actions;
  114. }
  115. Vec::new()
  116. }
  117. /// Check if the given condition matches.
  118. fn matches(
  119. &self,
  120. condition: Condition,
  121. user_id: Option<&str>,
  122. display_name: Option<&str>,
  123. ) -> bool {
  124. match self.match_condition(&condition, user_id, display_name) {
  125. Ok(true) => true,
  126. Ok(false) => false,
  127. Err(err) => {
  128. warn!("Condition match failed {err}");
  129. false
  130. }
  131. }
  132. }
  133. }
  134. impl PushRuleEvaluator {
  135. /// Match a given `Condition` for a push rule.
  136. pub fn match_condition(
  137. &self,
  138. condition: &Condition,
  139. user_id: Option<&str>,
  140. display_name: Option<&str>,
  141. ) -> Result<bool, Error> {
  142. let known_condition = match condition {
  143. Condition::Known(known) => known,
  144. Condition::Unknown(_) => {
  145. return Ok(false);
  146. }
  147. };
  148. let result = match known_condition {
  149. KnownCondition::EventMatch(event_match) => {
  150. self.match_event_match(event_match, user_id)?
  151. }
  152. KnownCondition::ContainsDisplayName => {
  153. if let Some(dn) = display_name {
  154. if !dn.is_empty() {
  155. get_glob_matcher(dn, GlobMatchType::Word)?.is_match(&self.body)?
  156. } else {
  157. // We specifically ignore empty display names, as otherwise
  158. // they would always match.
  159. false
  160. }
  161. } else {
  162. false
  163. }
  164. }
  165. KnownCondition::RoomMemberCount { is } => {
  166. if let Some(is) = is {
  167. self.match_member_count(is)?
  168. } else {
  169. false
  170. }
  171. }
  172. KnownCondition::SenderNotificationPermission { key } => {
  173. if let Some(sender_power_level) = &self.sender_power_level {
  174. let required_level = self
  175. .notification_power_levels
  176. .get(key.as_ref())
  177. .copied()
  178. .unwrap_or(50);
  179. *sender_power_level >= required_level
  180. } else {
  181. false
  182. }
  183. }
  184. KnownCondition::RelationMatch {
  185. rel_type,
  186. event_type_pattern,
  187. sender,
  188. sender_type,
  189. } => {
  190. self.match_relations(rel_type, sender, sender_type, user_id, event_type_pattern)?
  191. }
  192. };
  193. Ok(result)
  194. }
  195. /// Evaluates a relation condition.
  196. fn match_relations(
  197. &self,
  198. rel_type: &str,
  199. sender: &Option<Cow<str>>,
  200. sender_type: &Option<Cow<str>>,
  201. user_id: Option<&str>,
  202. event_type_pattern: &Option<Cow<str>>,
  203. ) -> Result<bool, Error> {
  204. // First check if relation matching is enabled...
  205. if !self.relation_match_enabled {
  206. return Ok(false);
  207. }
  208. // ... and if there are any relations to match against.
  209. let relations = if let Some(relations) = self.relations.get(rel_type) {
  210. relations
  211. } else {
  212. return Ok(false);
  213. };
  214. // Extract the sender pattern from the condition
  215. let sender_pattern = if let Some(sender) = sender {
  216. Some(sender.as_ref())
  217. } else if let Some(sender_type) = sender_type {
  218. if sender_type == "user_id" {
  219. if let Some(user_id) = user_id {
  220. Some(user_id)
  221. } else {
  222. return Ok(false);
  223. }
  224. } else {
  225. warn!("Unrecognized sender_type: {sender_type}");
  226. return Ok(false);
  227. }
  228. } else {
  229. None
  230. };
  231. let mut sender_compiled_pattern = if let Some(pattern) = sender_pattern {
  232. Some(get_glob_matcher(pattern, GlobMatchType::Whole)?)
  233. } else {
  234. None
  235. };
  236. let mut type_compiled_pattern = if let Some(pattern) = event_type_pattern {
  237. Some(get_glob_matcher(pattern, GlobMatchType::Whole)?)
  238. } else {
  239. None
  240. };
  241. for (relation_sender, event_type) in relations {
  242. if let Some(pattern) = &mut sender_compiled_pattern {
  243. if !pattern.is_match(relation_sender)? {
  244. continue;
  245. }
  246. }
  247. if let Some(pattern) = &mut type_compiled_pattern {
  248. if !pattern.is_match(event_type)? {
  249. continue;
  250. }
  251. }
  252. return Ok(true);
  253. }
  254. Ok(false)
  255. }
  256. /// Evaluates a `event_match` condition.
  257. fn match_event_match(
  258. &self,
  259. event_match: &EventMatchCondition,
  260. user_id: Option<&str>,
  261. ) -> Result<bool, Error> {
  262. let pattern = if let Some(pattern) = &event_match.pattern {
  263. pattern
  264. } else if let Some(pattern_type) = &event_match.pattern_type {
  265. // The `pattern_type` can either be "user_id" or "user_localpart",
  266. // either way if we don't have a `user_id` then the condition can't
  267. // match.
  268. let user_id = if let Some(user_id) = user_id {
  269. user_id
  270. } else {
  271. return Ok(false);
  272. };
  273. match &**pattern_type {
  274. "user_id" => user_id,
  275. "user_localpart" => get_localpart_from_id(user_id)?,
  276. _ => return Ok(false),
  277. }
  278. } else {
  279. return Ok(false);
  280. };
  281. let haystack = if let Some(haystack) = self.flattened_keys.get(&*event_match.key) {
  282. haystack
  283. } else {
  284. return Ok(false);
  285. };
  286. // For the content.body we match against "words", but for everything
  287. // else we match against the entire value.
  288. let match_type = if event_match.key == "content.body" {
  289. GlobMatchType::Word
  290. } else {
  291. GlobMatchType::Whole
  292. };
  293. let mut compiled_pattern = get_glob_matcher(pattern, match_type)?;
  294. compiled_pattern.is_match(haystack)
  295. }
  296. /// Match the member count against an 'is' condition
  297. /// The `is` condition can be things like '>2', '==3' or even just '4'.
  298. fn match_member_count(&self, is: &str) -> Result<bool, Error> {
  299. let captures = INEQUALITY_EXPR.captures(is).context("bad 'is' clause")?;
  300. let ineq = captures.get(1).map_or("==", |m| m.as_str());
  301. let rhs: u64 = captures
  302. .get(2)
  303. .context("missing number")?
  304. .as_str()
  305. .parse()?;
  306. let matches = match ineq {
  307. "" | "==" => self.room_member_count == rhs,
  308. "<" => self.room_member_count < rhs,
  309. ">" => self.room_member_count > rhs,
  310. ">=" => self.room_member_count >= rhs,
  311. "<=" => self.room_member_count <= rhs,
  312. _ => false,
  313. };
  314. Ok(matches)
  315. }
  316. }
  317. #[test]
  318. fn push_rule_evaluator() {
  319. let mut flattened_keys = BTreeMap::new();
  320. flattened_keys.insert("content.body".to_string(), "foo bar bob hello".to_string());
  321. let evaluator = PushRuleEvaluator::py_new(
  322. flattened_keys,
  323. 10,
  324. Some(0),
  325. BTreeMap::new(),
  326. BTreeMap::new(),
  327. true,
  328. )
  329. .unwrap();
  330. let result = evaluator.run(&FilteredPushRules::default(), None, Some("bob"));
  331. assert_eq!(result.len(), 3);
  332. }