use std::{
fmt::{Display, Formatter, Result as FmtResult},
str::FromStr,
};
use ruma_events_macros::ruma_event;
use serde::{
de::{Error, Visitor},
ser::SerializeStruct as _,
Deserialize, Deserializer, Serialize, Serializer,
};
use serde_json::{from_value, Value};
use crate::{util::default_true, FromStrError};
ruma_event! {
PushRulesEvent {
kind: Event,
event_type: "m.push_rules",
content: {
pub global: Ruleset,
},
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Ruleset {
pub content: Vec<PatternedPushRule>,
#[serde(rename = "override")]
pub override_rules: Vec<ConditionalPushRule>,
pub room: Vec<PushRule>,
pub sender: Vec<PushRule>,
pub underride: Vec<ConditionalPushRule>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct PushRule {
pub actions: Vec<Action>,
pub default: bool,
pub enabled: bool,
pub rule_id: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ConditionalPushRule {
pub actions: Vec<Action>,
pub default: bool,
pub enabled: bool,
pub rule_id: String,
pub conditions: Vec<PushCondition>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct PatternedPushRule {
pub actions: Vec<Action>,
pub default: bool,
pub enabled: bool,
pub rule_id: String,
pub pattern: String,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Action {
Notify,
DontNotify,
Coalesce,
SetTweak(Tweak),
#[doc(hidden)]
__Nonexhaustive,
}
impl Display for Action {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let variant = match *self {
Action::Notify => "notify",
Action::DontNotify => "dont_notify",
Action::Coalesce => "coalesce",
Action::SetTweak(_) => "set_tweak",
Action::__Nonexhaustive => {
panic!("__Nonexhaustive enum variant is not intended for use.")
}
};
write!(f, "{}", variant)
}
}
impl FromStr for Action {
type Err = FromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let action = match s {
"notify" => Action::Notify,
"dont_notify" => Action::DontNotify,
"coalesce" => Action::Coalesce,
_ => return Err(FromStrError),
};
Ok(action)
}
}
impl Serialize for Action {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
Action::Notify => serializer.serialize_str("notify"),
Action::DontNotify => serializer.serialize_str("dont_notify"),
Action::Coalesce => serializer.serialize_str("coalesce"),
Action::SetTweak(ref tweak) => tweak.serialize(serializer),
_ => panic!("Attempted to serialize __Nonexhaustive variant."),
}
}
}
impl<'de> Deserialize<'de> for Action {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrStruct;
impl<'de> Visitor<'de> for StringOrStruct {
type Value = Action;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("action as string or map")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match FromStr::from_str(value) {
Ok(action) => Ok(action),
Err(_) => Err(serde::de::Error::custom("not a string action")),
}
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
match Tweak::deserialize(serde::de::value::MapAccessDeserializer::new(map)) {
Ok(tweak) => Ok(Action::SetTweak(tweak)),
Err(_) => Err(serde::de::Error::custom("unknown action")),
}
}
}
deserializer.deserialize_any(StringOrStruct)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "set_tweak")]
pub enum Tweak {
#[serde(rename = "sound")]
Sound {
value: String,
},
#[serde(rename = "highlight")]
Highlight {
#[serde(default = "default_true")]
value: bool,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum PushCondition {
EventMatch(EventMatchCondition),
ContainsDisplayName,
RoomMemberCount(RoomMemberCountCondition),
SenderNotificationPermission(SenderNotificationPermissionCondition),
#[doc(hidden)]
__Nonexhaustive,
}
impl Serialize for PushCondition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
PushCondition::EventMatch(ref condition) => condition.serialize(serializer),
PushCondition::ContainsDisplayName => {
let mut state = serializer.serialize_struct("ContainsDisplayNameCondition", 1)?;
state.serialize_field("kind", "contains_display_name")?;
state.end()
}
PushCondition::RoomMemberCount(ref condition) => condition.serialize(serializer),
PushCondition::SenderNotificationPermission(ref condition) => {
condition.serialize(serializer)
}
PushCondition::__Nonexhaustive => {
panic!("__Nonexhaustive enum variant is not intended for use.");
}
}
}
}
impl<'de> Deserialize<'de> for PushCondition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value: Value = Deserialize::deserialize(deserializer)?;
let kind_value = match value.get("kind") {
Some(value) => value.clone(),
None => return Err(D::Error::missing_field("kind")),
};
let kind = match kind_value.as_str() {
Some(kind) => kind,
None => return Err(D::Error::custom("field `kind` must be a string")),
};
match kind {
"event_match" => {
let condition = match from_value::<EventMatchCondition>(value) {
Ok(condition) => condition,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(PushCondition::EventMatch(condition))
}
"contains_display_name" => Ok(PushCondition::ContainsDisplayName),
"room_member_count" => {
let condition = match from_value::<RoomMemberCountCondition>(value) {
Ok(condition) => condition,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(PushCondition::RoomMemberCount(condition))
}
"sender_notification_permission" => {
let condition = match from_value::<SenderNotificationPermissionCondition>(value) {
Ok(condition) => condition,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(PushCondition::SenderNotificationPermission(condition))
}
unknown_kind => Err(D::Error::custom(&format!(
"unknown condition kind `{}`",
unknown_kind
))),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct EventMatchCondition {
pub key: String,
pub pattern: String,
}
impl Serialize for EventMatchCondition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("EventMatchCondition", 3)?;
state.serialize_field("key", &self.key)?;
state.serialize_field("kind", "event_match")?;
state.serialize_field("pattern", &self.pattern)?;
state.end()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct RoomMemberCountCondition {
pub is: String,
}
impl Serialize for RoomMemberCountCondition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("RoomMemberCountCondition", 2)?;
state.serialize_field("is", &self.is)?;
state.serialize_field("kind", "room_member_count")?;
state.end()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct SenderNotificationPermissionCondition {
pub key: String,
}
impl Serialize for SenderNotificationPermissionCondition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("SenderNotificationPermissionCondition", 2)?;
state.serialize_field("key", &self.key)?;
state.serialize_field("kind", "sender_notification_permission")?;
state.end()
}
}
#[cfg(test)]
mod tests {
use serde_json::{from_str, to_string};
use super::{
Action, EventMatchCondition, PushCondition, PushRulesEvent, RoomMemberCountCondition,
SenderNotificationPermissionCondition, Tweak,
};
use crate::EventResult;
#[test]
fn serialize_string_action() {
assert_eq!(to_string(&Action::Notify).unwrap(), r#""notify""#);
}
#[test]
fn serialize_tweak_sound_action() {
assert_eq!(
to_string(&Action::SetTweak(Tweak::Sound {
value: "default".to_string()
}))
.unwrap(),
r#"{"set_tweak":"sound","value":"default"}"#
);
}
#[test]
fn serialize_tweak_highlight_action() {
assert_eq!(
to_string(&Action::SetTweak(Tweak::Highlight { value: true })).unwrap(),
r#"{"set_tweak":"highlight","value":true}"#
);
}
#[test]
fn deserialize_string_action() {
assert_eq!(from_str::<Action>(r#""notify""#).unwrap(), Action::Notify);
}
#[test]
fn deserialize_tweak_sound_action() {
assert_eq!(
from_str::<Action>(r#"{"set_tweak":"sound","value":"default"}"#).unwrap(),
Action::SetTweak(Tweak::Sound {
value: "default".to_string()
})
);
}
#[test]
fn deserialize_tweak_highlight_action() {
assert_eq!(
from_str::<Action>(r#"{"set_tweak":"highlight","value":true}"#).unwrap(),
Action::SetTweak(Tweak::Highlight { value: true })
);
}
#[test]
fn deserialize_tweak_highlight_action_with_default_value() {
assert_eq!(
from_str::<Action>(r#"{"set_tweak":"highlight"}"#).unwrap(),
Action::SetTweak(Tweak::Highlight { value: true })
);
}
#[test]
fn serialize_event_match_condition() {
assert_eq!(
to_string(&PushCondition::EventMatch(EventMatchCondition {
key: "content.msgtype".to_string(),
pattern: "m.notice".to_string(),
}))
.unwrap(),
r#"{"key":"content.msgtype","kind":"event_match","pattern":"m.notice"}"#
);
}
#[test]
fn serialize_contains_display_name_condition() {
assert_eq!(
to_string(&PushCondition::ContainsDisplayName).unwrap(),
r#"{"kind":"contains_display_name"}"#
);
}
#[test]
fn serialize_room_member_count_condition() {
assert_eq!(
to_string(&PushCondition::RoomMemberCount(RoomMemberCountCondition {
is: "2".to_string(),
}))
.unwrap(),
r#"{"is":"2","kind":"room_member_count"}"#
);
}
#[test]
fn serialize_sender_notification_permission_condition() {
assert_eq!(
r#"{"key":"room","kind":"sender_notification_permission"}"#,
to_string(&PushCondition::SenderNotificationPermission(
SenderNotificationPermissionCondition {
key: "room".to_string(),
}
))
.unwrap(),
);
}
#[test]
fn deserialize_event_match_condition() {
assert_eq!(
from_str::<PushCondition>(
r#"{"key":"content.msgtype","kind":"event_match","pattern":"m.notice"}"#
)
.unwrap(),
PushCondition::EventMatch(EventMatchCondition {
key: "content.msgtype".to_string(),
pattern: "m.notice".to_string(),
})
);
}
#[test]
fn deserialize_contains_display_name_condition() {
assert_eq!(
from_str::<PushCondition>(r#"{"kind":"contains_display_name"}"#).unwrap(),
PushCondition::ContainsDisplayName,
);
}
#[test]
fn deserialize_room_member_count_condition() {
assert_eq!(
from_str::<PushCondition>(r#"{"is":"2","kind":"room_member_count"}"#).unwrap(),
PushCondition::RoomMemberCount(RoomMemberCountCondition {
is: "2".to_string(),
})
);
}
#[test]
fn deserialize_sender_notification_permission_condition() {
assert_eq!(
from_str::<PushCondition>(r#"{"key":"room","kind":"sender_notification_permission"}"#)
.unwrap(),
PushCondition::SenderNotificationPermission(SenderNotificationPermissionCondition {
key: "room".to_string(),
})
);
}
#[test]
fn sanity_check() {
let json = r#"{
"content": {
"global": {
"content": [
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"default": true,
"enabled": true,
"pattern": "alice",
"rule_id": ".m.rule.contains_user_name"
}
],
"override": [
{
"actions": [
"dont_notify"
],
"conditions": [],
"default": true,
"enabled": false,
"rule_id": ".m.rule.master"
},
{
"actions": [
"dont_notify"
],
"conditions": [
{
"key": "content.msgtype",
"kind": "event_match",
"pattern": "m.notice"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.suppress_notices"
}
],
"room": [],
"sender": [],
"underride": [
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "ring"
},
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.call.invite"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.call"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"conditions": [
{
"kind": "contains_display_name"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.contains_display_name"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"is": "2",
"kind": "room_member_count"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.room_one_to_one"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
},
{
"key": "content.membership",
"kind": "event_match",
"pattern": "invite"
},
{
"key": "state_key",
"kind": "event_match",
"pattern": "@alice:example.com"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.invite_for_me"
},
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.member_event"
},
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.message"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.message"
}
]
}
},
"type": "m.push_rules"
}"#;
assert!(serde_json::from_str::<EventResult<PushRulesEvent>>(json)
.unwrap()
.into_result()
.is_ok());
}
}