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#[derive(Clone, Debug)]
20#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
21pub enum Action {
22 Notify,
24
25 #[cfg(feature = "unstable-msc3768")]
28 NotifyInApp,
29
30 SetTweak(Tweak),
32
33 #[doc(hidden)]
35 _Custom(CustomAction),
36}
37
38impl Action {
39 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 pub fn is_highlight(&self) -> bool {
69 matches!(self, Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
70 }
71
72 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 #[cfg(feature = "unstable-msc3768")]
84 pub fn should_notify_remote(&self) -> bool {
85 matches!(self, Action::Notify)
86 }
87
88 pub fn sound(&self) -> Option<&SoundTweakValue> {
90 as_variant!(self, Action::SetTweak(Tweak::Sound(sound)) => sound)
91 }
92
93 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#[doc(hidden)]
116#[derive(Debug, Clone, Serialize)]
117#[serde(transparent)]
118pub struct CustomAction(CustomActionData);
119
120#[allow(unknown_lints, unnameable_types)]
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(untagged)]
124pub enum CustomActionData {
125 String(String),
127
128 Object(JsonObject),
130}
131
132#[derive(Clone, Debug)]
134#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
135pub enum Tweak {
136 Sound(SoundTweakValue),
140
141 Highlight(HighlightTweakValue),
143
144 #[doc(hidden)]
145 _Custom(CustomTweak),
146}
147
148impl Tweak {
149 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 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 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#[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 Default,
231
232 #[doc(hidden)]
233 _Custom(PrivOwnedStr),
234}
235
236#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
242#[allow(clippy::exhaustive_enums)]
243pub enum HighlightTweakValue {
244 #[default]
246 Yes,
247
248 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#[doc(hidden)]
260#[derive(Clone, Debug, Serialize)]
261pub struct CustomTweak {
262 set_tweak: String,
264
265 #[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 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 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}