push: Add support for intentional mentions push rules

According to MSC3952
This commit is contained in:
Kévin Commaille 2023-05-24 13:05:37 +02:00 committed by Kévin Commaille
parent f8ed83aa53
commit 766fba75f9
4 changed files with 312 additions and 2 deletions

View File

@ -42,6 +42,7 @@ Improvements:
- Add `MessageType::sanitize` behind the `unstable-sanitize` feature - Add `MessageType::sanitize` behind the `unstable-sanitize` feature
- Add `MatrixVersion::V1_7` - Add `MatrixVersion::V1_7`
- Stabilize support for annotations and reactions (MSC2677 / Matrix 1.7) - Stabilize support for annotations and reactions (MSC2677 / Matrix 1.7)
- Add support for intentional mentions push rules (MSC3952 / Matrix 1.7)
# 0.11.3 # 0.11.3

View File

@ -484,6 +484,7 @@ impl ConditionalPushRule {
#[cfg(feature = "unstable-msc3932")] #[cfg(feature = "unstable-msc3932")]
{ {
// These 3 rules always apply. // These 3 rules always apply.
#[allow(deprecated)]
if self.rule_id != PredefinedOverrideRuleId::Master.as_ref() if self.rule_id != PredefinedOverrideRuleId::Master.as_ref()
&& self.rule_id != PredefinedOverrideRuleId::RoomNotif.as_ref() && self.rule_id != PredefinedOverrideRuleId::RoomNotif.as_ref()
&& self.rule_id != PredefinedOverrideRuleId::ContainsDisplayName.as_ref() && self.rule_id != PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
@ -503,6 +504,15 @@ impl ConditionalPushRule {
} }
} }
// The old mention rules are disabled when an m.mentions field is present.
#[allow(deprecated)]
if (self.rule_id == PredefinedOverrideRuleId::RoomNotif.as_ref()
|| self.rule_id == PredefinedOverrideRuleId::ContainsDisplayName.as_ref())
&& event.contains_mentions()
{
return false;
}
self.conditions.iter().all(|cond| cond.applies(event, context)) self.conditions.iter().all(|cond| cond.applies(event, context))
} }
} }
@ -601,6 +611,14 @@ impl PatternedPushRule {
event: &FlattenedJson, event: &FlattenedJson,
context: &PushConditionRoomCtx, context: &PushConditionRoomCtx,
) -> bool { ) -> bool {
// The old mention rules are disabled when an m.mentions field is present.
#[allow(deprecated)]
if self.rule_id == PredefinedContentRuleId::ContainsUserName.as_ref()
&& event.contains_mentions()
{
return false;
}
if event.get_str("sender").map_or(false, |sender| sender == context.user_id) { if event.get_str("sender").map_or(false, |sender| sender == context.user_id) {
return false; return false;
} }
@ -971,7 +989,13 @@ mod tests {
condition::{PushCondition, PushConditionRoomCtx, RoomMemberCountIs}, condition::{PushCondition, PushConditionRoomCtx, RoomMemberCountIs},
AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule, AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule,
}; };
use crate::{power_levels::NotificationPowerLevels, room_id, serde::Raw, user_id}; use crate::{
power_levels::NotificationPowerLevels,
push::{PredefinedContentRuleId, PredefinedOverrideRuleId},
room_id,
serde::Raw,
user_id,
};
fn example_ruleset() -> Ruleset { fn example_ruleset() -> Ruleset {
let mut set = Ruleset::new(); let mut set = Ruleset::new();
@ -1652,4 +1676,172 @@ mod tests {
); );
assert_eq!(sound, "three"); assert_eq!(sound, "three");
} }
#[test]
#[allow(deprecated)]
fn old_mentions_apply() {
let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
let context = &PushConditionRoomCtx {
room_id: room_id!("!far_west:server.name").to_owned(),
member_count: uint!(100),
user_id: user_id!("@jj:server.name").to_owned(),
user_display_name: "Jolly Jumper".into(),
users_power_levels: BTreeMap::new(),
default_power_level: int!(50),
notification_power_levels: NotificationPowerLevels { room: int!(50) },
#[cfg(feature = "unstable-msc3931")]
supported_features: Default::default(),
};
let message = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "jolly_jumper"
},
"type": "m.room.message"
}"#,
)
.unwrap();
assert_eq!(
set.get_match(&message, context).unwrap().rule_id(),
PredefinedContentRuleId::ContainsUserName.as_ref()
);
let message = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "jolly_jumper",
"m.mentions": {}
},
"type": "m.room.message"
}"#,
)
.unwrap();
assert_ne!(
set.get_match(&message, context).unwrap().rule_id(),
PredefinedContentRuleId::ContainsUserName.as_ref()
);
let message = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "Jolly Jumper"
},
"type": "m.room.message"
}"#,
)
.unwrap();
assert_eq!(
set.get_match(&message, context).unwrap().rule_id(),
PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
);
let message = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "Jolly Jumper",
"m.mentions": {}
},
"type": "m.room.message"
}"#,
)
.unwrap();
assert_ne!(
set.get_match(&message, context).unwrap().rule_id(),
PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
);
let message = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "@room"
},
"sender": "@admin:server.name",
"type": "m.room.message"
}"#,
)
.unwrap();
assert_eq!(
set.get_match(&message, context).unwrap().rule_id(),
PredefinedOverrideRuleId::RoomNotif.as_ref()
);
let message = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "@room",
"m.mentions": {}
},
"sender": "@admin:server.name",
"type": "m.room.message"
}"#,
)
.unwrap();
assert_ne!(
set.get_match(&message, context).unwrap().rule_id(),
PredefinedOverrideRuleId::RoomNotif.as_ref()
);
}
#[test]
fn intentional_mentions_apply() {
let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
let context = &PushConditionRoomCtx {
room_id: room_id!("!far_west:server.name").to_owned(),
member_count: uint!(100),
user_id: user_id!("@jj:server.name").to_owned(),
user_display_name: "Jolly Jumper".into(),
users_power_levels: BTreeMap::new(),
default_power_level: int!(50),
notification_power_levels: NotificationPowerLevels { room: int!(50) },
#[cfg(feature = "unstable-msc3931")]
supported_features: Default::default(),
};
let message = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "Hey jolly_jumper!",
"m.mentions": {
"user_ids": ["@jolly_jumper:server.name"]
}
},
"sender": "@admin:server.name",
"type": "m.room.message"
}"#,
)
.unwrap();
assert_eq!(
set.get_match(&message, context).unwrap().rule_id(),
PredefinedOverrideRuleId::IsUserMention.as_ref()
);
let message = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "Listen room!",
"m.mentions": {
"room": true
}
},
"sender": "@admin:server.name",
"type": "m.room.message"
}"#,
)
.unwrap();
assert_eq!(
set.get_match(&message, context).unwrap().rule_id(),
PredefinedOverrideRuleId::IsRoomMention.as_ref()
);
}
} }

View File

@ -58,6 +58,13 @@ impl FlattenedJson {
pub fn get_str(&self, path: &str) -> Option<&str> { pub fn get_str(&self, path: &str) -> Option<&str> {
self.map.get(path).and_then(|v| v.as_str()) self.map.get(path).and_then(|v| v.as_str())
} }
/// Whether this flattened JSON contains an `m.mentions` property under the `content` property.
pub fn contains_mentions(&self) -> bool {
self.map
.keys()
.any(|s| s == r"content.m\.mentions" || s.starts_with(r"content.m\.mentions."))
}
} }
/// Escape a key for path matching. /// Escape a key for path matching.
@ -382,4 +389,48 @@ mod tests {
}, },
); );
} }
#[test]
fn contains_mentions() {
let raw = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"m.mentions": {},
"content": {
"body": "Text"
}
}"#,
)
.unwrap();
let flattened = FlattenedJson::from_raw(&raw);
assert!(!flattened.contains_mentions());
let raw = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "Text",
"m.mentions": {}
}
}"#,
)
.unwrap();
let flattened = FlattenedJson::from_raw(&raw);
assert!(flattened.contains_mentions());
let raw = serde_json::from_str::<Raw<JsonValue>>(
r#"{
"content": {
"body": "Text",
"m.mentions": {
"room": true
}
}
}"#,
)
.unwrap();
let flattened = FlattenedJson::from_raw(&raw);
assert!(flattened.contains_mentions());
}
} }

View File

@ -21,13 +21,21 @@ impl Ruleset {
/// [predefined push rules]: https://spec.matrix.org/latest/client-server-api/#predefined-rules /// [predefined push rules]: https://spec.matrix.org/latest/client-server-api/#predefined-rules
pub fn server_default(user_id: &UserId) -> Self { pub fn server_default(user_id: &UserId) -> Self {
Self { Self {
content: [PatternedPushRule::contains_user_name(user_id)].into(), content: [
#[allow(deprecated)]
PatternedPushRule::contains_user_name(user_id),
]
.into(),
override_: [ override_: [
ConditionalPushRule::master(), ConditionalPushRule::master(),
ConditionalPushRule::suppress_notices(), ConditionalPushRule::suppress_notices(),
ConditionalPushRule::invite_for_me(user_id), ConditionalPushRule::invite_for_me(user_id),
ConditionalPushRule::member_event(), ConditionalPushRule::member_event(),
ConditionalPushRule::is_user_mention(user_id),
#[allow(deprecated)]
ConditionalPushRule::contains_display_name(), ConditionalPushRule::contains_display_name(),
ConditionalPushRule::is_room_mention(),
#[allow(deprecated)]
ConditionalPushRule::roomnotif(), ConditionalPushRule::roomnotif(),
ConditionalPushRule::tombstone(), ConditionalPushRule::tombstone(),
ConditionalPushRule::reaction(), ConditionalPushRule::reaction(),
@ -179,9 +187,33 @@ impl ConditionalPushRule {
} }
} }
/// Matches any message which contains the users Matrix ID in the list of `user_ids` under the
/// `m.mentions` property.
pub fn is_user_mention(user_id: &UserId) -> Self {
Self {
actions: vec![
Notify,
SetTweak(Tweak::Sound("default".to_owned())),
SetTweak(Tweak::Highlight(true)),
],
default: true,
enabled: true,
rule_id: PredefinedOverrideRuleId::IsUserMention.to_string(),
conditions: vec![EventPropertyContains {
key: r"content.m\.mentions.user_ids".to_owned(),
value: user_id.as_str().into(),
}],
}
}
/// Matches any message whose content is unencrypted and contains the user's current display /// Matches any message whose content is unencrypted and contains the user's current display
/// name in the room in which it was sent. /// name in the room in which it was sent.
///
/// Since Matrix 1.7, this rule only matches if the event's content does not contain an
/// `m.mentions` property.
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with ConditionalPushRule::is_user_mention() instead."]
pub fn contains_display_name() -> Self { pub fn contains_display_name() -> Self {
#[allow(deprecated)]
Self { Self {
actions: vec![ actions: vec![
Notify, Notify,
@ -211,9 +243,29 @@ impl ConditionalPushRule {
} }
} }
/// Matches any message from a sender with the proper power level with the `room` property of
/// the `m.mentions` property set to `true`.
pub fn is_room_mention() -> Self {
Self {
actions: vec![Notify, SetTweak(Tweak::Highlight(true))],
default: true,
enabled: true,
rule_id: PredefinedOverrideRuleId::IsRoomMention.to_string(),
conditions: vec![
EventPropertyIs { key: r"content.m\.mentions.room".to_owned(), value: true.into() },
SenderNotificationPermission { key: "room".to_owned() },
],
}
}
/// Matches any message whose content is unencrypted and contains the text `@room`, signifying /// Matches any message whose content is unencrypted and contains the text `@room`, signifying
/// the whole room should be notified of the event. /// the whole room should be notified of the event.
///
/// Since Matrix 1.7, this rule only matches if the event's content does not contain an
/// `m.mentions` property.
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with ConditionalPushRule::is_room_mention() instead."]
pub fn roomnotif() -> Self { pub fn roomnotif() -> Self {
#[allow(deprecated)]
Self { Self {
actions: vec![Notify, SetTweak(Tweak::Highlight(true))], actions: vec![Notify, SetTweak(Tweak::Highlight(true))],
default: true, default: true,
@ -260,7 +312,12 @@ impl ConditionalPushRule {
impl PatternedPushRule { impl PatternedPushRule {
/// Matches any message whose content is unencrypted and contains the local part of the user's /// Matches any message whose content is unencrypted and contains the local part of the user's
/// Matrix ID, separated by word boundaries. /// Matrix ID, separated by word boundaries.
///
/// Since Matrix 1.7, this rule only matches if the event's content does not contain an
/// `m.mentions` property.
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with ConditionalPushRule::is_user_mention() instead."]
pub fn contains_user_name(user_id: &UserId) -> Self { pub fn contains_user_name(user_id: &UserId) -> Self {
#[allow(deprecated)]
Self { Self {
rule_id: PredefinedContentRuleId::ContainsUserName.to_string(), rule_id: PredefinedContentRuleId::ContainsUserName.to_string(),
enabled: true, enabled: true,
@ -453,11 +510,19 @@ pub enum PredefinedOverrideRuleId {
/// `.m.rule.member_event` /// `.m.rule.member_event`
MemberEvent, MemberEvent,
/// `.m.rule.is_user_mention`
IsUserMention,
/// `.m.rule.contains_display_name` /// `.m.rule.contains_display_name`
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with PredefinedOverrideRuleId::IsUserMention instead."]
ContainsDisplayName, ContainsDisplayName,
/// `.m.rule.is_room_mention`
IsRoomMention,
/// `.m.rule.roomnotif` /// `.m.rule.roomnotif`
#[ruma_enum(rename = ".m.rule.roomnotif")] #[ruma_enum(rename = ".m.rule.roomnotif")]
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with PredefinedOverrideRuleId::IsRoomMention instead."]
RoomNotif, RoomNotif,
/// `.m.rule.tombstone` /// `.m.rule.tombstone`
@ -522,6 +587,7 @@ pub enum PredefinedUnderrideRuleId {
#[non_exhaustive] #[non_exhaustive]
pub enum PredefinedContentRuleId { pub enum PredefinedContentRuleId {
/// `.m.rule.contains_user_name` /// `.m.rule.contains_user_name`
#[deprecated = "Since Matrix 1.7. Use the m.mentions property with PredefinedOverrideRuleId::IsUserMention instead."]
ContainsUserName, ContainsUserName,
#[doc(hidden)] #[doc(hidden)]