1use std::borrow::Cow;
4
5use ruma_macros::StringEnum;
6#[cfg(feature = "unstable-msc4426")]
7use serde::Deserialize;
8use serde::Serialize;
9use serde_json::{Value as JsonValue, from_value as from_json_value, to_value as to_json_value};
10
11#[cfg(feature = "unstable-msc4426")]
12use crate::SecondsSinceUnixEpoch;
13use crate::{OwnedMxcUri, PrivOwnedStr};
14
15mod profile_field_value_serde;
16mod static_profile_field;
17mod user_profile;
18
19#[doc(hidden)]
20pub use self::profile_field_value_serde::ProfileFieldValueVisitor;
21pub use self::{static_profile_field::*, user_profile::*};
22
23#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
27#[derive(Clone, StringEnum)]
28#[ruma_enum(rename_all = "snake_case")]
29#[non_exhaustive]
30pub enum ProfileFieldName {
31 AvatarUrl,
33
34 #[ruma_enum(rename = "displayname")]
36 DisplayName,
37
38 #[ruma_enum(rename = "m.tz")]
40 TimeZone,
41
42 #[cfg(feature = "unstable-msc4426")]
46 #[ruma_enum(rename = "org.matrix.msc4426.status")]
47 Status,
48
49 #[cfg(feature = "unstable-msc4426")]
53 #[ruma_enum(rename = "org.matrix.msc4426.call")]
54 Call,
55
56 #[doc(hidden)]
57 _Custom(PrivOwnedStr),
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64#[serde(rename_all = "snake_case")]
65#[non_exhaustive]
66pub enum ProfileFieldValue {
67 AvatarUrl(OwnedMxcUri),
69
70 #[serde(rename = "displayname")]
72 DisplayName(String),
73
74 #[serde(rename = "m.tz")]
76 TimeZone(String),
77
78 #[cfg(feature = "unstable-msc4426")]
82 #[serde(rename = "org.matrix.msc4426.status")]
83 Status(StatusProfileField),
84
85 #[cfg(feature = "unstable-msc4426")]
89 #[serde(rename = "org.matrix.msc4426.call")]
90 Call(CallProfileField),
91
92 #[doc(hidden)]
93 #[serde(untagged)]
94 _Custom(CustomProfileFieldValue),
95}
96
97impl ProfileFieldValue {
98 pub fn new(field: &str, value: JsonValue) -> serde_json::Result<Self> {
109 Ok(match field {
110 "avatar_url" => Self::AvatarUrl(from_json_value(value)?),
111 "displayname" => Self::DisplayName(from_json_value(value)?),
112 "m.tz" => Self::TimeZone(from_json_value(value)?),
113 _ => Self::_Custom(CustomProfileFieldValue { field: field.to_owned(), value }),
114 })
115 }
116
117 pub fn field_name(&self) -> ProfileFieldName {
119 match self {
120 Self::AvatarUrl(_) => ProfileFieldName::AvatarUrl,
121 Self::DisplayName(_) => ProfileFieldName::DisplayName,
122 Self::TimeZone(_) => ProfileFieldName::TimeZone,
123 #[cfg(feature = "unstable-msc4426")]
124 Self::Status(_) => ProfileFieldName::Status,
125 #[cfg(feature = "unstable-msc4426")]
126 Self::Call(_) => ProfileFieldName::Call,
127 Self::_Custom(CustomProfileFieldValue { field, .. }) => field.as_str().into(),
128 }
129 }
130
131 pub fn value(&self) -> Cow<'_, JsonValue> {
136 match self {
137 Self::AvatarUrl(value) => {
138 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
139 }
140 Self::DisplayName(value) => {
141 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
142 }
143 Self::TimeZone(value) => {
144 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
145 }
146 #[cfg(feature = "unstable-msc4426")]
147 Self::Status(value) => {
148 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
149 }
150 #[cfg(feature = "unstable-msc4426")]
151 Self::Call(value) => {
152 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
153 }
154 Self::_Custom(c) => Cow::Borrowed(&c.value),
155 }
156 }
157}
158
159#[cfg(feature = "unstable-msc4426")]
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[non_exhaustive]
165pub struct StatusProfileField {
166 pub text: String,
170
171 pub emoji: String,
175}
176
177#[cfg(feature = "unstable-msc4426")]
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[non_exhaustive]
182pub struct CallProfileField {
183 #[serde(skip_serializing_if = "Option::is_none")]
187 pub call_joined_ts: Option<SecondsSinceUnixEpoch>,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192#[doc(hidden)]
193pub struct CustomProfileFieldValue {
194 field: String,
196
197 value: JsonValue,
199}
200
201#[cfg(test)]
202mod tests {
203 use ruma_common::{canonical_json::assert_to_canonical_json_eq, owned_mxc_uri};
204 use serde_json::{from_value as from_json_value, json};
205
206 use super::ProfileFieldValue;
207 #[cfg(feature = "unstable-msc4426")]
208 use super::{CallProfileField, StatusProfileField};
209 #[cfg(feature = "unstable-msc4426")]
210 use crate::SecondsSinceUnixEpoch;
211
212 #[test]
213 fn serialize_profile_field_value() {
214 let value = ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef"));
216 assert_to_canonical_json_eq!(value, json!({ "avatar_url": "mxc://localhost/abcdef" }));
217
218 let value = ProfileFieldValue::DisplayName("Alice".to_owned());
220 assert_to_canonical_json_eq!(value, json!({ "displayname": "Alice" }));
221
222 let value = ProfileFieldValue::new("custom_field", "value".into()).unwrap();
224 assert_to_canonical_json_eq!(value, json!({ "custom_field": "value" }));
225 }
226
227 #[test]
228 fn deserialize_profile_field_value() {
229 let json = json!({ "avatar_url": "mxc://localhost/abcdef" });
231 assert_eq!(
232 from_json_value::<ProfileFieldValue>(json).unwrap(),
233 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef"))
234 );
235
236 let json = json!({ "displayname": "Alice" });
238 assert_eq!(
239 from_json_value::<ProfileFieldValue>(json).unwrap(),
240 ProfileFieldValue::DisplayName("Alice".to_owned())
241 );
242
243 let json = json!({ "custom_field": "value" });
245 let value = from_json_value::<ProfileFieldValue>(json).unwrap();
246 assert_eq!(value.field_name().as_str(), "custom_field");
247 assert_eq!(value.value().as_str(), Some("value"));
248
249 let json = json!({});
251 from_json_value::<ProfileFieldValue>(json).unwrap_err();
252 }
253
254 #[test]
255 #[cfg(feature = "unstable-msc4426")]
256 fn serialize_profile_status() {
257 let value = ProfileFieldValue::Status(StatusProfileField {
259 text: "Away".to_owned(),
260 emoji: "🌴".to_owned(),
261 });
262 assert_to_canonical_json_eq!(
263 value,
264 json!({ "org.matrix.msc4426.status": { "text": "Away", "emoji": "🌴" } })
265 );
266
267 let value = ProfileFieldValue::Call(CallProfileField {
269 call_joined_ts: Some(SecondsSinceUnixEpoch(1_770_140_640.try_into().unwrap())),
270 });
271 assert_to_canonical_json_eq!(
272 value,
273 json!({ "org.matrix.msc4426.call": { "call_joined_ts": 1_770_140_640 } })
274 );
275 }
276
277 #[test]
278 #[cfg(feature = "unstable-msc4426")]
279 fn deserialize_profile_status() {
280 let json =
282 json!({ "org.matrix.msc4426.status": { "text": "Be right back", "emoji": "☕️" } });
283 assert_eq!(
284 from_json_value::<ProfileFieldValue>(json).unwrap(),
285 ProfileFieldValue::Status(StatusProfileField {
286 text: "Be right back".to_owned(),
287 emoji: "☕️".to_owned(),
288 })
289 );
290
291 let json = json!({ "org.matrix.msc4426.call": { "call_joined_ts": 1_168_380_060 } });
293 assert_eq!(
294 from_json_value::<ProfileFieldValue>(json).unwrap(),
295 ProfileFieldValue::Call(CallProfileField {
296 call_joined_ts: Some(SecondsSinceUnixEpoch(1_168_380_060.try_into().unwrap())),
297 })
298 );
299 }
300}