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_matches::assert_matches;
273 use serde_json::{from_value as from_json_value, json};
274 use strass::{assert_let, assert_variant_eq};
275
276 use super::{Action, HighlightTweakValue, SoundTweakValue, Tweak};
277 use crate::{assert_to_canonical_json_eq, push::action::CustomActionData};
278
279 #[test]
280 fn serialize_notify() {
281 assert_to_canonical_json_eq!(Action::Notify, json!("notify"));
282 }
283
284 #[cfg(feature = "unstable-msc3768")]
285 #[test]
286 fn serialize_notify_in_app() {
287 assert_to_canonical_json_eq!(
288 Action::NotifyInApp,
289 json!("org.matrix.msc3768.notify_in_app"),
290 );
291 }
292
293 #[test]
294 fn serialize_tweak_sound() {
295 assert_to_canonical_json_eq!(
296 Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
297 json!({ "set_tweak": "sound", "value": "default" })
298 );
299 }
300
301 #[test]
302 fn serialize_tweak_highlight() {
303 assert_to_canonical_json_eq!(
304 Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
305 json!({ "set_tweak": "highlight" })
306 );
307
308 assert_to_canonical_json_eq!(
309 Action::SetTweak(Tweak::Highlight(HighlightTweakValue::No)),
310 json!({ "set_tweak": "highlight", "value": false })
311 );
312 }
313
314 #[test]
315 fn deserialize_notify() {
316 assert_matches!(from_json_value::<Action>(json!("notify")), Ok(Action::Notify));
317 }
318
319 #[cfg(feature = "unstable-msc3768")]
320 #[test]
321 fn deserialize_notify_in_app() {
322 assert_matches!(
323 from_json_value::<Action>(json!("org.matrix.msc3768.notify_in_app")),
324 Ok(Action::NotifyInApp)
325 );
326 }
327
328 #[test]
329 fn deserialize_tweak_sound() {
330 let json_data = json!({
331 "set_tweak": "sound",
332 "value": "default"
333 });
334 assert_let!(
335 Ok(Action::SetTweak(Tweak::Sound(value))) = from_json_value::<Action>(json_data)
336 );
337 assert_eq!(value, SoundTweakValue::Default);
338
339 let json_data = json!({
340 "set_tweak": "sound",
341 "value": "custom"
342 });
343 assert_let!(
344 Ok(Action::SetTweak(Tweak::Sound(value))) = from_json_value::<Action>(json_data)
345 );
346 assert_eq!(value.as_str(), "custom");
347 }
348
349 #[test]
350 fn deserialize_tweak_highlight() {
351 let json_data = json!({
352 "set_tweak": "highlight",
353 "value": true
354 });
355 assert_matches!(
356 from_json_value::<Action>(json_data),
357 Ok(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
358 );
359 }
360
361 #[test]
362 fn deserialize_tweak_highlight_with_default_value() {
363 assert_matches!(
364 from_json_value::<Action>(json!({ "set_tweak": "highlight" })),
365 Ok(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
366 );
367 }
368
369 #[test]
370 fn custom_tweak_serialize_roundtrip() {
371 let json = json!({ "set_tweak": "dev.local.tweak", "value": "rainbow" });
372 let tweak = from_json_value::<Tweak>(json.clone()).unwrap();
373
374 assert_eq!(tweak.set_tweak(), "dev.local.tweak");
375 assert_eq!(tweak.value().unwrap().get(), r#""rainbow""#);
376
377 assert_to_canonical_json_eq!(tweak, json);
378 }
379
380 #[test]
381 fn custom_action_serialize_roundtrip() {
382 let json = json!("dev.local.action");
384 let action = from_json_value::<Action>(json.clone()).unwrap();
385 assert_variant_eq!(&*action.data(), CustomActionData::String("dev.local.action"));
386 assert_to_canonical_json_eq!(action, json);
387
388 let json = json!({ "dev.local.action": "rainbow" });
390 let action = from_json_value::<Action>(json.clone()).unwrap();
391 assert_let!(CustomActionData::Object(value) = &*action.data());
392 assert_eq!(value.len(), 1);
393 assert_eq!(value["dev.local.action"], "rainbow");
394 assert_to_canonical_json_eq!(action, json);
395 }
396}