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.
 
 
 
 
 
 

773 lines
26 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. //! An implementation of Matrix push rules.
  15. //!
  16. //! The `Cow<_>` type is used extensively within this module to allow creating
  17. //! the base rules as constants (in Rust constants can't require explicit
  18. //! allocation atm).
  19. //!
  20. //! ---
  21. //!
  22. //! Push rules is the system used to determine which events trigger a push (and a
  23. //! bump in notification counts).
  24. //!
  25. //! This consists of a list of "push rules" for each user, where a push rule is a
  26. //! pair of "conditions" and "actions". When a user receives an event Synapse
  27. //! iterates over the list of push rules until it finds one where all the conditions
  28. //! match the event, at which point "actions" describe the outcome (e.g. notify,
  29. //! highlight, etc).
  30. //!
  31. //! Push rules are split up into 5 different "kinds" (aka "priority classes"), which
  32. //! are run in order:
  33. //! 1. Override — highest priority rules, e.g. always ignore notices
  34. //! 2. Content — content specific rules, e.g. @ notifications
  35. //! 3. Room — per room rules, e.g. enable/disable notifications for all messages
  36. //! in a room
  37. //! 4. Sender — per sender rules, e.g. never notify for messages from a given
  38. //! user
  39. //! 5. Underride — the lowest priority "default" rules, e.g. notify for every
  40. //! message.
  41. //!
  42. //! The set of "base rules" are the list of rules that every user has by default. A
  43. //! user can modify their copy of the push rules in one of three ways:
  44. //! 1. Adding a new push rule of a certain kind
  45. //! 2. Changing the actions of a base rule
  46. //! 3. Enabling/disabling a base rule.
  47. //!
  48. //! The base rules are split into whether they come before or after a particular
  49. //! kind, so the order of push rule evaluation would be: base rules for before
  50. //! "override" kind, user defined "override" rules, base rules after "override"
  51. //! kind, etc, etc.
  52. use std::borrow::Cow;
  53. use std::collections::{BTreeMap, HashMap, HashSet};
  54. use anyhow::{Context, Error};
  55. use log::warn;
  56. use pyo3::exceptions::PyTypeError;
  57. use pyo3::prelude::*;
  58. use pyo3::types::{PyBool, PyList, PyLong, PyString};
  59. use pythonize::{depythonize, pythonize};
  60. use serde::de::Error as _;
  61. use serde::{Deserialize, Serialize};
  62. use serde_json::Value;
  63. use self::evaluator::PushRuleEvaluator;
  64. mod base_rules;
  65. pub mod evaluator;
  66. pub mod utils;
  67. /// Called when registering modules with python.
  68. pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
  69. let child_module = PyModule::new(py, "push")?;
  70. child_module.add_class::<PushRule>()?;
  71. child_module.add_class::<PushRules>()?;
  72. child_module.add_class::<FilteredPushRules>()?;
  73. child_module.add_class::<PushRuleEvaluator>()?;
  74. child_module.add_function(wrap_pyfunction!(get_base_rule_ids, m)?)?;
  75. m.add_submodule(child_module)?;
  76. // We need to manually add the module to sys.modules to make `from
  77. // synapse.synapse_rust import push` work.
  78. py.import("sys")?
  79. .getattr("modules")?
  80. .set_item("synapse.synapse_rust.push", child_module)?;
  81. Ok(())
  82. }
  83. #[pyfunction]
  84. fn get_base_rule_ids() -> HashSet<&'static str> {
  85. base_rules::BASE_RULES_BY_ID.keys().copied().collect()
  86. }
  87. /// A single push rule for a user.
  88. #[derive(Debug, Clone)]
  89. #[pyclass(frozen)]
  90. pub struct PushRule {
  91. /// A unique ID for this rule
  92. pub rule_id: Cow<'static, str>,
  93. /// The "kind" of push rule this is (see `PRIORITY_CLASS_MAP` in Python)
  94. #[pyo3(get)]
  95. pub priority_class: i32,
  96. /// The conditions that must all match for actions to be applied
  97. pub conditions: Cow<'static, [Condition]>,
  98. /// The actions to apply if all conditions are met
  99. pub actions: Cow<'static, [Action]>,
  100. /// Whether this is a base rule
  101. #[pyo3(get)]
  102. pub default: bool,
  103. /// Whether this is enabled by default
  104. #[pyo3(get)]
  105. pub default_enabled: bool,
  106. }
  107. #[pymethods]
  108. impl PushRule {
  109. #[staticmethod]
  110. pub fn from_db(
  111. rule_id: String,
  112. priority_class: i32,
  113. conditions: &str,
  114. actions: &str,
  115. ) -> Result<PushRule, Error> {
  116. let conditions = serde_json::from_str(conditions).context("parsing conditions")?;
  117. let actions = serde_json::from_str(actions).context("parsing actions")?;
  118. Ok(PushRule {
  119. rule_id: Cow::Owned(rule_id),
  120. priority_class,
  121. conditions,
  122. actions,
  123. default: false,
  124. default_enabled: true,
  125. })
  126. }
  127. #[getter]
  128. fn rule_id(&self) -> &str {
  129. &self.rule_id
  130. }
  131. #[getter]
  132. fn actions(&self) -> Vec<Action> {
  133. self.actions.clone().into_owned()
  134. }
  135. #[getter]
  136. fn conditions(&self) -> Vec<Condition> {
  137. self.conditions.clone().into_owned()
  138. }
  139. fn __repr__(&self) -> String {
  140. format!(
  141. "<PushRule rule_id={}, conditions={:?}, actions={:?}>",
  142. self.rule_id, self.conditions, self.actions
  143. )
  144. }
  145. }
  146. /// The "action" Synapse should perform for a matching push rule.
  147. #[derive(Debug, Clone, PartialEq, Eq)]
  148. pub enum Action {
  149. Notify,
  150. SetTweak(SetTweak),
  151. // Legacy actions that should be understood, but are equivalent to no-ops.
  152. DontNotify,
  153. Coalesce,
  154. // An unrecognized custom action.
  155. Unknown(Value),
  156. }
  157. impl IntoPy<PyObject> for Action {
  158. fn into_py(self, py: Python<'_>) -> PyObject {
  159. // When we pass the `Action` struct to Python we want it to be converted
  160. // to a dict. We use `pythonize`, which converts the struct using the
  161. // `serde` serialization.
  162. pythonize(py, &self).expect("valid action")
  163. }
  164. }
  165. /// The body of a `SetTweak` push action.
  166. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
  167. pub struct SetTweak {
  168. set_tweak: Cow<'static, str>,
  169. #[serde(skip_serializing_if = "Option::is_none")]
  170. value: Option<TweakValue>,
  171. // This picks up any other fields that may have been added by clients.
  172. // These get added when we convert the `Action` to a python object.
  173. #[serde(flatten)]
  174. other_keys: Value,
  175. }
  176. /// The value of a `set_tweak`.
  177. ///
  178. /// We need this (rather than using `TweakValue` directly) so that we can use
  179. /// `&'static str` in the value when defining the constant base rules.
  180. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
  181. #[serde(untagged)]
  182. pub enum TweakValue {
  183. String(Cow<'static, str>),
  184. Other(Value),
  185. }
  186. impl Serialize for Action {
  187. fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  188. where
  189. S: serde::Serializer,
  190. {
  191. match self {
  192. Action::DontNotify => serializer.serialize_str("dont_notify"),
  193. Action::Notify => serializer.serialize_str("notify"),
  194. Action::Coalesce => serializer.serialize_str("coalesce"),
  195. Action::SetTweak(tweak) => tweak.serialize(serializer),
  196. Action::Unknown(value) => value.serialize(serializer),
  197. }
  198. }
  199. }
  200. /// Simple helper class for deserializing Action from JSON.
  201. #[derive(Deserialize)]
  202. #[serde(untagged)]
  203. enum ActionDeserializeHelper {
  204. Str(String),
  205. SetTweak(SetTweak),
  206. Unknown(Value),
  207. }
  208. impl<'de> Deserialize<'de> for Action {
  209. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  210. where
  211. D: serde::Deserializer<'de>,
  212. {
  213. let helper: ActionDeserializeHelper = Deserialize::deserialize(deserializer)?;
  214. match helper {
  215. ActionDeserializeHelper::Str(s) => match &*s {
  216. "dont_notify" => Ok(Action::DontNotify),
  217. "notify" => Ok(Action::Notify),
  218. "coalesce" => Ok(Action::Coalesce),
  219. _ => Err(D::Error::custom("unrecognized action")),
  220. },
  221. ActionDeserializeHelper::SetTweak(set_tweak) => Ok(Action::SetTweak(set_tweak)),
  222. ActionDeserializeHelper::Unknown(value) => Ok(Action::Unknown(value)),
  223. }
  224. }
  225. }
  226. /// A simple JSON values (string, int, boolean, or null).
  227. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
  228. #[serde(untagged)]
  229. pub enum SimpleJsonValue {
  230. Str(Cow<'static, str>),
  231. Int(i64),
  232. Bool(bool),
  233. Null,
  234. }
  235. impl<'source> FromPyObject<'source> for SimpleJsonValue {
  236. fn extract(ob: &'source PyAny) -> PyResult<Self> {
  237. if let Ok(s) = <PyString as pyo3::PyTryFrom>::try_from(ob) {
  238. Ok(SimpleJsonValue::Str(Cow::Owned(s.to_string())))
  239. // A bool *is* an int, ensure we try bool first.
  240. } else if let Ok(b) = <PyBool as pyo3::PyTryFrom>::try_from(ob) {
  241. Ok(SimpleJsonValue::Bool(b.extract()?))
  242. } else if let Ok(i) = <PyLong as pyo3::PyTryFrom>::try_from(ob) {
  243. Ok(SimpleJsonValue::Int(i.extract()?))
  244. } else if ob.is_none() {
  245. Ok(SimpleJsonValue::Null)
  246. } else {
  247. Err(PyTypeError::new_err(format!(
  248. "Can't convert from {} to SimpleJsonValue",
  249. ob.get_type().name()?
  250. )))
  251. }
  252. }
  253. }
  254. /// A JSON values (list, string, int, boolean, or null).
  255. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
  256. #[serde(untagged)]
  257. pub enum JsonValue {
  258. Array(Vec<SimpleJsonValue>),
  259. Value(SimpleJsonValue),
  260. }
  261. impl<'source> FromPyObject<'source> for JsonValue {
  262. fn extract(ob: &'source PyAny) -> PyResult<Self> {
  263. if let Ok(l) = <PyList as pyo3::PyTryFrom>::try_from(ob) {
  264. match l.iter().map(SimpleJsonValue::extract).collect() {
  265. Ok(a) => Ok(JsonValue::Array(a)),
  266. Err(e) => Err(PyTypeError::new_err(format!(
  267. "Can't convert to JsonValue::Array: {e}"
  268. ))),
  269. }
  270. } else if let Ok(v) = SimpleJsonValue::extract(ob) {
  271. Ok(JsonValue::Value(v))
  272. } else {
  273. Err(PyTypeError::new_err(format!(
  274. "Can't convert from {} to JsonValue",
  275. ob.get_type().name()?
  276. )))
  277. }
  278. }
  279. }
  280. /// A condition used in push rules to match against an event.
  281. ///
  282. /// We need this split as `serde` doesn't give us the ability to have a
  283. /// "catchall" variant in tagged enums.
  284. #[derive(Serialize, Deserialize, Debug, Clone)]
  285. #[serde(untagged)]
  286. pub enum Condition {
  287. /// A recognized condition that we can match against
  288. Known(KnownCondition),
  289. /// An unrecognized condition that we ignore.
  290. Unknown(Value),
  291. }
  292. /// The set of "known" conditions that we can handle.
  293. #[derive(Serialize, Deserialize, Debug, Clone)]
  294. #[serde(rename_all = "snake_case")]
  295. #[serde(tag = "kind")]
  296. pub enum KnownCondition {
  297. EventMatch(EventMatchCondition),
  298. // Identical to event_match but gives predefined patterns. Cannot be added by users.
  299. #[serde(skip_deserializing, rename = "event_match")]
  300. EventMatchType(EventMatchTypeCondition),
  301. EventPropertyIs(EventPropertyIsCondition),
  302. #[serde(rename = "im.nheko.msc3664.related_event_match")]
  303. RelatedEventMatch(RelatedEventMatchCondition),
  304. // Identical to related_event_match but gives predefined patterns. Cannot be added by users.
  305. #[serde(skip_deserializing, rename = "im.nheko.msc3664.related_event_match")]
  306. RelatedEventMatchType(RelatedEventMatchTypeCondition),
  307. EventPropertyContains(EventPropertyIsCondition),
  308. // Identical to exact_event_property_contains but gives predefined patterns. Cannot be added by users.
  309. #[serde(skip_deserializing, rename = "event_property_contains")]
  310. ExactEventPropertyContainsType(EventPropertyIsTypeCondition),
  311. ContainsDisplayName,
  312. RoomMemberCount {
  313. #[serde(skip_serializing_if = "Option::is_none")]
  314. is: Option<Cow<'static, str>>,
  315. },
  316. SenderNotificationPermission {
  317. key: Cow<'static, str>,
  318. },
  319. #[serde(rename = "org.matrix.msc3931.room_version_supports")]
  320. RoomVersionSupports {
  321. feature: Cow<'static, str>,
  322. },
  323. }
  324. impl IntoPy<PyObject> for Condition {
  325. fn into_py(self, py: Python<'_>) -> PyObject {
  326. pythonize(py, &self).expect("valid condition")
  327. }
  328. }
  329. impl<'source> FromPyObject<'source> for Condition {
  330. fn extract(ob: &'source PyAny) -> PyResult<Self> {
  331. Ok(depythonize(ob)?)
  332. }
  333. }
  334. /// The body of a [`Condition::EventMatch`] with a pattern.
  335. #[derive(Serialize, Deserialize, Debug, Clone)]
  336. pub struct EventMatchCondition {
  337. pub key: Cow<'static, str>,
  338. pub pattern: Cow<'static, str>,
  339. }
  340. #[derive(Serialize, Debug, Clone)]
  341. #[serde(rename_all = "snake_case")]
  342. pub enum EventMatchPatternType {
  343. UserId,
  344. UserLocalpart,
  345. }
  346. /// The body of a [`Condition::EventMatch`] that uses user_id or user_localpart as a pattern.
  347. #[derive(Serialize, Debug, Clone)]
  348. pub struct EventMatchTypeCondition {
  349. pub key: Cow<'static, str>,
  350. // During serialization, the pattern_type property gets replaced with a
  351. // pattern property of the correct value in synapse.push.clientformat.format_push_rules_for_user.
  352. pub pattern_type: Cow<'static, EventMatchPatternType>,
  353. }
  354. /// The body of a [`Condition::EventPropertyIs`]
  355. #[derive(Serialize, Deserialize, Debug, Clone)]
  356. pub struct EventPropertyIsCondition {
  357. pub key: Cow<'static, str>,
  358. pub value: Cow<'static, SimpleJsonValue>,
  359. }
  360. /// The body of a [`Condition::EventPropertyIs`] that uses user_id or user_localpart as a pattern.
  361. #[derive(Serialize, Debug, Clone)]
  362. pub struct EventPropertyIsTypeCondition {
  363. pub key: Cow<'static, str>,
  364. // During serialization, the pattern_type property gets replaced with a
  365. // pattern property of the correct value in synapse.push.clientformat.format_push_rules_for_user.
  366. pub value_type: Cow<'static, EventMatchPatternType>,
  367. }
  368. /// The body of a [`Condition::RelatedEventMatch`]
  369. #[derive(Serialize, Deserialize, Debug, Clone)]
  370. pub struct RelatedEventMatchCondition {
  371. #[serde(skip_serializing_if = "Option::is_none")]
  372. pub key: Option<Cow<'static, str>>,
  373. #[serde(skip_serializing_if = "Option::is_none")]
  374. pub pattern: Option<Cow<'static, str>>,
  375. pub rel_type: Cow<'static, str>,
  376. #[serde(skip_serializing_if = "Option::is_none")]
  377. pub include_fallbacks: Option<bool>,
  378. }
  379. /// The body of a [`Condition::RelatedEventMatch`] that uses user_id or user_localpart as a pattern.
  380. #[derive(Serialize, Debug, Clone)]
  381. pub struct RelatedEventMatchTypeCondition {
  382. // This is only used if pattern_type exists (and thus key must exist), so is
  383. // a bit simpler than RelatedEventMatchCondition.
  384. pub key: Cow<'static, str>,
  385. pub pattern_type: Cow<'static, EventMatchPatternType>,
  386. pub rel_type: Cow<'static, str>,
  387. #[serde(skip_serializing_if = "Option::is_none")]
  388. pub include_fallbacks: Option<bool>,
  389. }
  390. /// The collection of push rules for a user.
  391. #[derive(Debug, Clone, Default)]
  392. #[pyclass(frozen)]
  393. pub struct PushRules {
  394. /// Custom push rules that override a base rule.
  395. overridden_base_rules: HashMap<Cow<'static, str>, PushRule>,
  396. /// Custom rules that come between the prepend/append override base rules.
  397. override_rules: Vec<PushRule>,
  398. /// Custom rules that come before the base content rules.
  399. content: Vec<PushRule>,
  400. /// Custom rules that come before the base room rules.
  401. room: Vec<PushRule>,
  402. /// Custom rules that come before the base sender rules.
  403. sender: Vec<PushRule>,
  404. /// Custom rules that come before the base underride rules.
  405. underride: Vec<PushRule>,
  406. }
  407. #[pymethods]
  408. impl PushRules {
  409. #[new]
  410. pub fn new(rules: Vec<PushRule>) -> PushRules {
  411. let mut push_rules: PushRules = Default::default();
  412. for rule in rules {
  413. if let Some(&o) = base_rules::BASE_RULES_BY_ID.get(&*rule.rule_id) {
  414. push_rules.overridden_base_rules.insert(
  415. rule.rule_id.clone(),
  416. PushRule {
  417. actions: rule.actions.clone(),
  418. ..o.clone()
  419. },
  420. );
  421. continue;
  422. }
  423. match rule.priority_class {
  424. 5 => push_rules.override_rules.push(rule),
  425. 4 => push_rules.content.push(rule),
  426. 3 => push_rules.room.push(rule),
  427. 2 => push_rules.sender.push(rule),
  428. 1 => push_rules.underride.push(rule),
  429. _ => {
  430. warn!(
  431. "Unrecognized priority class for rule {}: {}",
  432. rule.rule_id, rule.priority_class
  433. );
  434. }
  435. }
  436. }
  437. push_rules
  438. }
  439. /// Returns the list of all rules, including base rules, in the order they
  440. /// should be executed in.
  441. fn rules(&self) -> Vec<PushRule> {
  442. self.iter().cloned().collect()
  443. }
  444. }
  445. impl PushRules {
  446. /// Iterates over all the rules, including base rules, in the order they
  447. /// should be executed in.
  448. pub fn iter(&self) -> impl Iterator<Item = &PushRule> {
  449. base_rules::BASE_PREPEND_OVERRIDE_RULES
  450. .iter()
  451. .chain(self.override_rules.iter())
  452. .chain(base_rules::BASE_APPEND_OVERRIDE_RULES.iter())
  453. .chain(self.content.iter())
  454. .chain(base_rules::BASE_APPEND_CONTENT_RULES.iter())
  455. .chain(self.room.iter())
  456. .chain(self.sender.iter())
  457. .chain(self.underride.iter())
  458. .chain(base_rules::BASE_APPEND_UNDERRIDE_RULES.iter())
  459. .map(|rule| {
  460. self.overridden_base_rules
  461. .get(&*rule.rule_id)
  462. .unwrap_or(rule)
  463. })
  464. }
  465. }
  466. /// A wrapper around `PushRules` that checks the enabled state of rules and
  467. /// filters out disabled experimental rules.
  468. #[derive(Debug, Clone, Default)]
  469. #[pyclass(frozen)]
  470. pub struct FilteredPushRules {
  471. push_rules: PushRules,
  472. enabled_map: BTreeMap<String, bool>,
  473. msc1767_enabled: bool,
  474. msc3381_polls_enabled: bool,
  475. msc3664_enabled: bool,
  476. msc4028_push_encrypted_events: bool,
  477. }
  478. #[pymethods]
  479. impl FilteredPushRules {
  480. #[new]
  481. pub fn py_new(
  482. push_rules: PushRules,
  483. enabled_map: BTreeMap<String, bool>,
  484. msc1767_enabled: bool,
  485. msc3381_polls_enabled: bool,
  486. msc3664_enabled: bool,
  487. msc4028_push_encrypted_events: bool,
  488. ) -> Self {
  489. Self {
  490. push_rules,
  491. enabled_map,
  492. msc1767_enabled,
  493. msc3381_polls_enabled,
  494. msc3664_enabled,
  495. msc4028_push_encrypted_events,
  496. }
  497. }
  498. /// Returns the list of all rules and their enabled state, including base
  499. /// rules, in the order they should be executed in.
  500. fn rules(&self) -> Vec<(PushRule, bool)> {
  501. self.iter().map(|(r, e)| (r.clone(), e)).collect()
  502. }
  503. }
  504. impl FilteredPushRules {
  505. /// Iterates over all the rules and their enabled state, including base
  506. /// rules, in the order they should be executed in.
  507. fn iter(&self) -> impl Iterator<Item = (&PushRule, bool)> {
  508. self.push_rules
  509. .iter()
  510. .filter(|rule| {
  511. // Ignore disabled experimental push rules
  512. if !self.msc1767_enabled
  513. && (rule.rule_id.contains("org.matrix.msc1767")
  514. || rule.rule_id.contains("org.matrix.msc3933"))
  515. {
  516. return false;
  517. }
  518. if !self.msc3664_enabled
  519. && rule.rule_id == "global/override/.im.nheko.msc3664.reply"
  520. {
  521. return false;
  522. }
  523. if !self.msc3381_polls_enabled && rule.rule_id.contains("org.matrix.msc3930") {
  524. return false;
  525. }
  526. if !self.msc4028_push_encrypted_events
  527. && rule.rule_id == "global/override/.org.matrix.msc4028.encrypted_event"
  528. {
  529. return false;
  530. }
  531. true
  532. })
  533. .map(|r| {
  534. let enabled = *self
  535. .enabled_map
  536. .get(&*r.rule_id)
  537. .unwrap_or(&r.default_enabled);
  538. (r, enabled)
  539. })
  540. }
  541. }
  542. #[test]
  543. fn test_serialize_condition() {
  544. let condition = Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
  545. key: "content.body".into(),
  546. pattern: "coffee".into(),
  547. }));
  548. let json = serde_json::to_string(&condition).unwrap();
  549. assert_eq!(
  550. json,
  551. r#"{"kind":"event_match","key":"content.body","pattern":"coffee"}"#
  552. )
  553. }
  554. #[test]
  555. fn test_deserialize_condition() {
  556. let json = r#"{"kind":"event_match","key":"content.body","pattern":"coffee"}"#;
  557. let condition: Condition = serde_json::from_str(json).unwrap();
  558. assert!(matches!(
  559. condition,
  560. Condition::Known(KnownCondition::EventMatch(_))
  561. ));
  562. }
  563. #[test]
  564. fn test_serialize_event_match_condition_with_pattern_type() {
  565. let condition = Condition::Known(KnownCondition::EventMatchType(EventMatchTypeCondition {
  566. key: "content.body".into(),
  567. pattern_type: Cow::Owned(EventMatchPatternType::UserId),
  568. }));
  569. let json = serde_json::to_string(&condition).unwrap();
  570. assert_eq!(
  571. json,
  572. r#"{"kind":"event_match","key":"content.body","pattern_type":"user_id"}"#
  573. )
  574. }
  575. #[test]
  576. fn test_cannot_deserialize_event_match_condition_with_pattern_type() {
  577. let json = r#"{"kind":"event_match","key":"content.body","pattern_type":"user_id"}"#;
  578. let condition: Condition = serde_json::from_str(json).unwrap();
  579. assert!(matches!(condition, Condition::Unknown(_)));
  580. }
  581. #[test]
  582. fn test_deserialize_unstable_msc3664_condition() {
  583. let json = r#"{"kind":"im.nheko.msc3664.related_event_match","key":"content.body","pattern":"coffee","rel_type":"m.in_reply_to"}"#;
  584. let condition: Condition = serde_json::from_str(json).unwrap();
  585. assert!(matches!(
  586. condition,
  587. Condition::Known(KnownCondition::RelatedEventMatch(_))
  588. ));
  589. }
  590. #[test]
  591. fn test_serialize_unstable_msc3664_condition_with_pattern_type() {
  592. let condition = Condition::Known(KnownCondition::RelatedEventMatchType(
  593. RelatedEventMatchTypeCondition {
  594. key: "content.body".into(),
  595. pattern_type: Cow::Owned(EventMatchPatternType::UserId),
  596. rel_type: "m.in_reply_to".into(),
  597. include_fallbacks: Some(true),
  598. },
  599. ));
  600. let json = serde_json::to_string(&condition).unwrap();
  601. assert_eq!(
  602. json,
  603. r#"{"kind":"im.nheko.msc3664.related_event_match","key":"content.body","pattern_type":"user_id","rel_type":"m.in_reply_to","include_fallbacks":true}"#
  604. )
  605. }
  606. #[test]
  607. fn test_cannot_deserialize_unstable_msc3664_condition_with_pattern_type() {
  608. let json = r#"{"kind":"im.nheko.msc3664.related_event_match","key":"content.body","pattern_type":"user_id","rel_type":"m.in_reply_to"}"#;
  609. let condition: Condition = serde_json::from_str(json).unwrap();
  610. // Since pattern is optional on RelatedEventMatch it deserializes it to that
  611. // instead of RelatedEventMatchType.
  612. assert!(matches!(
  613. condition,
  614. Condition::Known(KnownCondition::RelatedEventMatch(_))
  615. ));
  616. }
  617. #[test]
  618. fn test_deserialize_unstable_msc3931_condition() {
  619. let json =
  620. r#"{"kind":"org.matrix.msc3931.room_version_supports","feature":"org.example.feature"}"#;
  621. let condition: Condition = serde_json::from_str(json).unwrap();
  622. assert!(matches!(
  623. condition,
  624. Condition::Known(KnownCondition::RoomVersionSupports { feature: _ })
  625. ));
  626. }
  627. #[test]
  628. fn test_deserialize_event_property_is_condition() {
  629. // A string condition should work.
  630. let json = r#"{"kind":"event_property_is","key":"content.value","value":"foo"}"#;
  631. let condition: Condition = serde_json::from_str(json).unwrap();
  632. assert!(matches!(
  633. condition,
  634. Condition::Known(KnownCondition::EventPropertyIs(_))
  635. ));
  636. // A boolean condition should work.
  637. let json = r#"{"kind":"event_property_is","key":"content.value","value":true}"#;
  638. let condition: Condition = serde_json::from_str(json).unwrap();
  639. assert!(matches!(
  640. condition,
  641. Condition::Known(KnownCondition::EventPropertyIs(_))
  642. ));
  643. // An integer condition should work.
  644. let json = r#"{"kind":"event_property_is","key":"content.value","value":1}"#;
  645. let condition: Condition = serde_json::from_str(json).unwrap();
  646. assert!(matches!(
  647. condition,
  648. Condition::Known(KnownCondition::EventPropertyIs(_))
  649. ));
  650. // A null condition should work
  651. let json = r#"{"kind":"event_property_is","key":"content.value","value":null}"#;
  652. let condition: Condition = serde_json::from_str(json).unwrap();
  653. assert!(matches!(
  654. condition,
  655. Condition::Known(KnownCondition::EventPropertyIs(_))
  656. ));
  657. }
  658. #[test]
  659. fn test_deserialize_custom_condition() {
  660. let json = r#"{"kind":"custom_tag"}"#;
  661. let condition: Condition = serde_json::from_str(json).unwrap();
  662. assert!(matches!(condition, Condition::Unknown(_)));
  663. let new_json = serde_json::to_string(&condition).unwrap();
  664. assert_eq!(json, new_json);
  665. }
  666. #[test]
  667. fn test_deserialize_action() {
  668. let _: Action = serde_json::from_str(r#""notify""#).unwrap();
  669. let _: Action = serde_json::from_str(r#""dont_notify""#).unwrap();
  670. let _: Action = serde_json::from_str(r#""coalesce""#).unwrap();
  671. let _: Action = serde_json::from_str(r#"{"set_tweak": "highlight"}"#).unwrap();
  672. }
  673. #[test]
  674. fn test_custom_action() {
  675. let json = r#"{"some_custom":"action_fields"}"#;
  676. let action: Action = serde_json::from_str(json).unwrap();
  677. assert!(matches!(action, Action::Unknown(_)));
  678. let new_json = serde_json::to_string(&action).unwrap();
  679. assert_eq!(json, new_json);
  680. }