Skip to main content

ruma_common/push/
action.rs

1use std::borrow::Cow;
2
3use as_variant::as_variant;
4use ruma_macros::StringEnum;
5use serde::{Deserialize, Serialize, de};
6use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
7
8mod action_serde;
9
10use crate::{
11    PrivOwnedStr,
12    serde::{JsonObject, from_raw_json_value},
13};
14
15/// This represents the different actions that should be taken when a rule is matched, and
16/// controls how notifications are delivered to the client.
17///
18/// See [the spec](https://spec.matrix.org/v1.18/client-server-api/#actions) for details.
19#[derive(Clone, Debug)]
20#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
21pub enum Action {
22    /// Causes matching events to generate a notification (both in-app and remote / push).
23    Notify,
24
25    /// Causes matching events to generate an in-app notification but no remote / push
26    /// notification.
27    #[cfg(feature = "unstable-msc3768")]
28    NotifyInApp,
29
30    /// Sets an entry in the 'tweaks' dictionary sent to the push gateway.
31    SetTweak(Tweak),
32
33    /// An unknown action.
34    #[doc(hidden)]
35    _Custom(CustomAction),
36}
37
38impl Action {
39    /// Creates a new `Action`.
40    ///
41    /// Prefer to use the public variants of `Action` where possible; this constructor is meant
42    /// be used for unsupported actions only and does not allow setting arbitrary data for
43    /// supported ones.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the action type is known and deserialization of `data` to the
48    /// corresponding variant fails.
49    pub fn new(data: CustomActionData) -> serde_json::Result<Self> {
50        Ok(match data {
51            CustomActionData::String(s) => match s.as_str() {
52                "notify" => Self::Notify,
53                #[cfg(feature = "unstable-msc3768")]
54                "org.matrix.msc3768.notify_in_app" => Self::NotifyInApp,
55                _ => Self::_Custom(CustomAction(CustomActionData::String(s))),
56            },
57            CustomActionData::Object(o) => {
58                if o.contains_key("set_tweak") {
59                    Self::SetTweak(serde_json::from_value(o.into())?)
60                } else {
61                    Self::_Custom(CustomAction(CustomActionData::Object(o)))
62                }
63            }
64        })
65    }
66
67    /// Whether this action is an `Action::SetTweak(Tweak::Highlight(true))`.
68    pub fn is_highlight(&self) -> bool {
69        matches!(self, Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
70    }
71
72    /// Whether this action should trigger a notification (either in-app or remote / push).
73    pub fn should_notify(&self) -> bool {
74        match self {
75            Action::Notify => true,
76            #[cfg(feature = "unstable-msc3768")]
77            Action::NotifyInApp => true,
78            _ => false,
79        }
80    }
81
82    /// Whether this action should trigger a remote / push notification.
83    #[cfg(feature = "unstable-msc3768")]
84    pub fn should_notify_remote(&self) -> bool {
85        matches!(self, Action::Notify)
86    }
87
88    /// The sound that should be played with this action, if any.
89    pub fn sound(&self) -> Option<&SoundTweakValue> {
90        as_variant!(self, Action::SetTweak(Tweak::Sound(sound)) => sound)
91    }
92
93    /// Access the data of this action.
94    pub fn data(&self) -> Cow<'_, CustomActionData> {
95        fn serialize<T: Serialize>(obj: T) -> JsonObject {
96            match serde_json::to_value(obj).expect("action serialization to succeed") {
97                JsonValue::Object(obj) => obj,
98                _ => panic!("action variant must serialize to object"),
99            }
100        }
101
102        match self {
103            Self::Notify => Cow::Owned(CustomActionData::String("notify".to_owned())),
104            #[cfg(feature = "unstable-msc3768")]
105            Self::NotifyInApp => {
106                Cow::Owned(CustomActionData::String("org.matrix.msc3768.notify_in_app".to_owned()))
107            }
108            Self::SetTweak(t) => Cow::Owned(CustomActionData::Object(serialize(t))),
109            Self::_Custom(c) => Cow::Borrowed(&c.0),
110        }
111    }
112}
113
114/// A custom action.
115#[doc(hidden)]
116#[derive(Debug, Clone, Serialize)]
117#[serde(transparent)]
118pub struct CustomAction(CustomActionData);
119
120/// The data of a custom action.
121#[allow(unknown_lints, unnameable_types)]
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(untagged)]
124pub enum CustomActionData {
125    /// A string.
126    String(String),
127
128    /// An object.
129    Object(JsonObject),
130}
131
132/// The `set_tweak` action.
133#[derive(Clone, Debug)]
134#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
135pub enum Tweak {
136    /// The sound to be played when this notification arrives.
137    ///
138    /// A device may choose to alert the user by some other means if appropriate, eg. vibration.
139    Sound(SoundTweakValue),
140
141    /// A boolean representing whether or not this message should be highlighted in the UI.
142    Highlight(HighlightTweakValue),
143
144    #[doc(hidden)]
145    _Custom(CustomTweak),
146}
147
148impl Tweak {
149    /// Creates a new `Tweak`.
150    ///
151    /// Prefer to use the public variants of `Tweak` where possible; this constructor is meant
152    /// be used for unsupported tweaks only and does not allow setting arbitrary data for
153    /// supported ones.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the `set_tweak` is known and deserialization of `value` to the
158    /// corresponding variant fails.
159    pub fn new(set_tweak: String, value: Option<Box<RawJsonValue>>) -> serde_json::Result<Self> {
160        Ok(match set_tweak.as_str() {
161            "sound" => Self::Sound(from_raw_json_value(
162                &value.ok_or_else(|| de::Error::missing_field("value"))?,
163            )?),
164            "highlight" => {
165                let value =
166                    value.map(|value| from_raw_json_value::<bool, _>(&value)).transpose()?;
167
168                let highlight = if value.is_none_or(|value| value) {
169                    HighlightTweakValue::Yes
170                } else {
171                    HighlightTweakValue::No
172                };
173
174                Self::Highlight(highlight)
175            }
176            _ => Self::_Custom(CustomTweak { set_tweak, value }),
177        })
178    }
179
180    /// Access the `set_tweak` value.
181    pub fn set_tweak(&self) -> &str {
182        match self {
183            Self::Sound(_) => "sound",
184            Self::Highlight(_) => "highlight",
185            Self::_Custom(CustomTweak { set_tweak, .. }) => set_tweak,
186        }
187    }
188
189    /// Access the value of this tweak.
190    ///
191    /// Prefer to use the public variants of `Tweak` where possible; this method is meant to
192    /// be used for custom tweaks only.
193    pub fn value(&self) -> Option<Cow<'_, RawJsonValue>> {
194        fn serialize<T: Serialize>(val: &T) -> Box<RawJsonValue> {
195            RawJsonValue::from_string(
196                serde_json::to_string(val).expect("tweak serialization to succeed"),
197            )
198            .expect("serialized tweak should be valid JSON")
199        }
200
201        match self {
202            Tweak::Sound(s) => Some(Cow::Owned(serialize(s))),
203            Tweak::Highlight(h) => {
204                Some(Cow::Owned(serialize(&matches!(h, HighlightTweakValue::Yes))))
205            }
206            Tweak::_Custom(c) => c.value.as_deref().map(Cow::Borrowed),
207        }
208    }
209}
210
211impl From<SoundTweakValue> for Tweak {
212    fn from(value: SoundTweakValue) -> Self {
213        Self::Sound(value)
214    }
215}
216
217impl From<HighlightTweakValue> for Tweak {
218    fn from(value: HighlightTweakValue) -> Self {
219        Self::Highlight(value)
220    }
221}
222
223/// A sound to play when a notification arrives.
224#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
225#[derive(Clone, StringEnum)]
226#[ruma_enum(rename_all = "lowercase")]
227#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
228pub enum SoundTweakValue {
229    /// Play the default notification sound.
230    Default,
231
232    #[doc(hidden)]
233    _Custom(PrivOwnedStr),
234}
235
236/// Whether or not a message should be highlighted in the UI.
237///
238/// This will normally take the form of presenting the message in a different color and/or
239/// style. The UI might also be adjusted to draw particular attention to the room in which the
240/// event occurred.
241#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
242#[allow(clippy::exhaustive_enums)]
243pub enum HighlightTweakValue {
244    /// Highlight the message.
245    #[default]
246    Yes,
247
248    /// Don't highlight the message.
249    No,
250}
251
252impl From<bool> for HighlightTweakValue {
253    fn from(value: bool) -> Self {
254        if value { Self::Yes } else { Self::No }
255    }
256}
257
258/// A custom tweak.
259#[doc(hidden)]
260#[derive(Clone, Debug, Serialize)]
261pub struct CustomTweak {
262    /// The kind of the custom tweak.
263    set_tweak: String,
264
265    /// The value of the custom tweak.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    value: Option<Box<RawJsonValue>>,
268}
269
270#[cfg(test)]
271mod tests {
272    use assert_matches2::{assert_let, assert_matches};
273    use serde_json::{Value as JsonValue, from_value as from_json_value, json};
274
275    use super::{Action, HighlightTweakValue, SoundTweakValue, Tweak};
276    use crate::{assert_to_canonical_json_eq, push::action::CustomActionData};
277
278    #[test]
279    fn serialize_notify() {
280        assert_to_canonical_json_eq!(Action::Notify, json!("notify"));
281    }
282
283    #[cfg(feature = "unstable-msc3768")]
284    #[test]
285    fn serialize_notify_in_app() {
286        assert_to_canonical_json_eq!(
287            Action::NotifyInApp,
288            json!("org.matrix.msc3768.notify_in_app"),
289        );
290    }
291
292    #[test]
293    fn serialize_tweak_sound() {
294        assert_to_canonical_json_eq!(
295            Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
296            json!({ "set_tweak": "sound", "value": "default" })
297        );
298    }
299
300    #[test]
301    fn serialize_tweak_highlight() {
302        assert_to_canonical_json_eq!(
303            Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
304            json!({ "set_tweak": "highlight" })
305        );
306
307        assert_to_canonical_json_eq!(
308            Action::SetTweak(Tweak::Highlight(HighlightTweakValue::No)),
309            json!({ "set_tweak": "highlight", "value": false })
310        );
311    }
312
313    #[test]
314    fn deserialize_notify() {
315        assert_matches!(from_json_value::<Action>(json!("notify")), Ok(Action::Notify));
316    }
317
318    #[cfg(feature = "unstable-msc3768")]
319    #[test]
320    fn deserialize_notify_in_app() {
321        assert_matches!(
322            from_json_value::<Action>(json!("org.matrix.msc3768.notify_in_app")),
323            Ok(Action::NotifyInApp)
324        );
325    }
326
327    #[test]
328    fn deserialize_tweak_sound() {
329        let json_data = json!({
330            "set_tweak": "sound",
331            "value": "default"
332        });
333        assert_matches!(
334            from_json_value::<Action>(json_data),
335            Ok(Action::SetTweak(Tweak::Sound(value)))
336        );
337        assert_eq!(value, SoundTweakValue::Default);
338
339        let json_data = json!({
340            "set_tweak": "sound",
341            "value": "custom"
342        });
343        assert_matches!(
344            from_json_value::<Action>(json_data),
345            Ok(Action::SetTweak(Tweak::Sound(value)))
346        );
347        assert_eq!(value.as_str(), "custom");
348    }
349
350    #[test]
351    fn deserialize_tweak_highlight() {
352        let json_data = json!({
353            "set_tweak": "highlight",
354            "value": true
355        });
356        assert_matches!(
357            from_json_value::<Action>(json_data),
358            Ok(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
359        );
360    }
361
362    #[test]
363    fn deserialize_tweak_highlight_with_default_value() {
364        assert_matches!(
365            from_json_value::<Action>(json!({ "set_tweak": "highlight" })),
366            Ok(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
367        );
368    }
369
370    #[test]
371    fn custom_tweak_serialize_roundtrip() {
372        let json = json!({ "set_tweak": "dev.local.tweak", "value": "rainbow" });
373        let tweak = from_json_value::<Tweak>(json.clone()).unwrap();
374
375        assert_eq!(tweak.set_tweak(), "dev.local.tweak");
376        assert_eq!(tweak.value().unwrap().get(), r#""rainbow""#);
377
378        assert_to_canonical_json_eq!(tweak, json);
379    }
380
381    #[test]
382    fn custom_action_serialize_roundtrip() {
383        // String action.
384        let json = json!("dev.local.action");
385        let action = from_json_value::<Action>(json.clone()).unwrap();
386        assert_let!(CustomActionData::String(value) = &*action.data());
387        assert_eq!(value, "dev.local.action");
388        assert_to_canonical_json_eq!(action, json);
389
390        // Object action.
391        let json = json!({ "dev.local.action": "rainbow" });
392        let action = from_json_value::<Action>(json.clone()).unwrap();
393        assert_let!(CustomActionData::Object(value) = &*action.data());
394        assert_eq!(value.len(), 1);
395        assert_let!(Some(JsonValue::String(s)) = value.get("dev.local.action"));
396        assert_eq!(s, "rainbow");
397        assert_to_canonical_json_eq!(action, json);
398    }
399}