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#[cfg(feature = "unstable-msc4262")]
19mod user_profile_update;
20
21#[doc(hidden)]
22pub use self::profile_field_value_serde::ProfileFieldValueVisitor;
23#[cfg(feature = "unstable-msc4262")]
24pub use self::user_profile_update::*;
25pub use self::{static_profile_field::*, user_profile::*};
26
27#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
31#[derive(Clone, StringEnum)]
32#[ruma_enum(rename_all = "snake_case")]
33#[non_exhaustive]
34pub enum ProfileFieldName {
35 AvatarUrl,
37
38 #[ruma_enum(rename = "displayname")]
40 DisplayName,
41
42 #[ruma_enum(rename = "m.tz")]
44 TimeZone,
45
46 #[cfg(feature = "unstable-msc4426")]
50 #[ruma_enum(rename = "org.matrix.msc4426.status")]
51 Status,
52
53 #[cfg(feature = "unstable-msc4426")]
57 #[ruma_enum(rename = "org.matrix.msc4426.call")]
58 Call,
59
60 #[doc(hidden)]
61 _Custom(PrivOwnedStr),
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
68#[serde(rename_all = "snake_case")]
69#[non_exhaustive]
70pub enum ProfileFieldValue {
71 AvatarUrl(OwnedMxcUri),
73
74 #[serde(rename = "displayname")]
76 DisplayName(String),
77
78 #[serde(rename = "m.tz")]
80 TimeZone(String),
81
82 #[cfg(feature = "unstable-msc4426")]
86 #[serde(rename = "org.matrix.msc4426.status")]
87 Status(StatusProfileField),
88
89 #[cfg(feature = "unstable-msc4426")]
93 #[serde(rename = "org.matrix.msc4426.call")]
94 Call(CallProfileField),
95
96 #[doc(hidden)]
97 #[serde(untagged)]
98 _Custom(CustomProfileFieldValue),
99}
100
101impl ProfileFieldValue {
102 pub fn new(field: &str, value: JsonValue) -> serde_json::Result<Self> {
113 Ok(match field {
114 "avatar_url" => Self::AvatarUrl(from_json_value(value)?),
115 "displayname" => Self::DisplayName(from_json_value(value)?),
116 "m.tz" => Self::TimeZone(from_json_value(value)?),
117 _ => Self::_Custom(CustomProfileFieldValue { field: field.to_owned(), value }),
118 })
119 }
120
121 pub fn field_name(&self) -> ProfileFieldName {
123 match self {
124 Self::AvatarUrl(_) => ProfileFieldName::AvatarUrl,
125 Self::DisplayName(_) => ProfileFieldName::DisplayName,
126 Self::TimeZone(_) => ProfileFieldName::TimeZone,
127 #[cfg(feature = "unstable-msc4426")]
128 Self::Status(_) => ProfileFieldName::Status,
129 #[cfg(feature = "unstable-msc4426")]
130 Self::Call(_) => ProfileFieldName::Call,
131 Self::_Custom(CustomProfileFieldValue { field, .. }) => field.as_str().into(),
132 }
133 }
134
135 pub fn value(&self) -> Cow<'_, JsonValue> {
140 match self {
141 Self::AvatarUrl(value) => {
142 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
143 }
144 Self::DisplayName(value) => {
145 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
146 }
147 Self::TimeZone(value) => {
148 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
149 }
150 #[cfg(feature = "unstable-msc4426")]
151 Self::Status(value) => {
152 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
153 }
154 #[cfg(feature = "unstable-msc4426")]
155 Self::Call(value) => {
156 Cow::Owned(to_json_value(value).expect("value should serialize successfully"))
157 }
158 Self::_Custom(c) => Cow::Borrowed(&c.value),
159 }
160 }
161}
162
163#[cfg(feature = "unstable-msc4426")]
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[non_exhaustive]
169pub struct StatusProfileField {
170 pub text: String,
174
175 pub emoji: String,
179}
180
181#[cfg(feature = "unstable-msc4426")]
182impl StatusProfileField {
183 pub fn new(text: String, emoji: String) -> Self {
185 Self { text, emoji }
186 }
187}
188
189#[cfg(feature = "unstable-msc4426")]
192#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[non_exhaustive]
194pub struct CallProfileField {
195 #[serde(skip_serializing_if = "Option::is_none")]
199 pub call_joined_ts: Option<SecondsSinceUnixEpoch>,
200}
201
202#[cfg(feature = "unstable-msc4426")]
203impl CallProfileField {
204 pub fn new() -> Self {
206 Self::default()
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
212#[doc(hidden)]
213pub struct CustomProfileFieldValue {
214 field: String,
216
217 value: JsonValue,
219}
220
221#[cfg(test)]
222mod tests {
223 use ruma_common::{canonical_json::assert_to_canonical_json_eq, owned_mxc_uri};
224 use serde_json::{from_value as from_json_value, json};
225
226 use super::ProfileFieldValue;
227 #[cfg(feature = "unstable-msc4426")]
228 use super::{CallProfileField, StatusProfileField};
229 #[cfg(feature = "unstable-msc4426")]
230 use crate::SecondsSinceUnixEpoch;
231
232 #[test]
233 fn serialize_profile_field_value() {
234 let value = ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef"));
236 assert_to_canonical_json_eq!(value, json!({ "avatar_url": "mxc://localhost/abcdef" }));
237
238 let value = ProfileFieldValue::DisplayName("Alice".to_owned());
240 assert_to_canonical_json_eq!(value, json!({ "displayname": "Alice" }));
241
242 let value = ProfileFieldValue::new("custom_field", "value".into()).unwrap();
244 assert_to_canonical_json_eq!(value, json!({ "custom_field": "value" }));
245 }
246
247 #[test]
248 fn deserialize_profile_field_value() {
249 let json = json!({ "avatar_url": "mxc://localhost/abcdef" });
251 assert_eq!(
252 from_json_value::<ProfileFieldValue>(json).unwrap(),
253 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef"))
254 );
255
256 let json = json!({ "displayname": "Alice" });
258 assert_eq!(
259 from_json_value::<ProfileFieldValue>(json).unwrap(),
260 ProfileFieldValue::DisplayName("Alice".to_owned())
261 );
262
263 let json = json!({ "custom_field": "value" });
265 let value = from_json_value::<ProfileFieldValue>(json).unwrap();
266 assert_eq!(value.field_name().as_str(), "custom_field");
267 assert_eq!(value.value().as_str(), Some("value"));
268
269 let json = json!({});
271 from_json_value::<ProfileFieldValue>(json).unwrap_err();
272 }
273
274 #[test]
275 #[cfg(feature = "unstable-msc4426")]
276 fn serialize_profile_status() {
277 let value =
279 ProfileFieldValue::Status(StatusProfileField::new("Away".to_owned(), "🌴".to_owned()));
280 assert_to_canonical_json_eq!(
281 value,
282 json!({ "org.matrix.msc4426.status": { "text": "Away", "emoji": "🌴" } })
283 );
284
285 let mut call = CallProfileField::new();
287 call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_770_140_640.try_into().unwrap()));
288 let value = ProfileFieldValue::Call(call);
289 assert_to_canonical_json_eq!(
290 value,
291 json!({ "org.matrix.msc4426.call": { "call_joined_ts": 1_770_140_640 } })
292 );
293 }
294
295 #[test]
296 #[cfg(feature = "unstable-msc4426")]
297 fn deserialize_profile_status() {
298 let json =
300 json!({ "org.matrix.msc4426.status": { "text": "Be right back", "emoji": "☕️" } });
301 assert_eq!(
302 from_json_value::<ProfileFieldValue>(json).unwrap(),
303 ProfileFieldValue::Status(StatusProfileField::new(
304 "Be right back".to_owned(),
305 "☕️".to_owned(),
306 ))
307 );
308
309 let json = json!({ "org.matrix.msc4426.call": { "call_joined_ts": 1_168_380_060 } });
311 let mut expected_call = CallProfileField::new();
312 expected_call.call_joined_ts =
313 Some(SecondsSinceUnixEpoch(1_168_380_060.try_into().unwrap()));
314 assert_eq!(
315 from_json_value::<ProfileFieldValue>(json).unwrap(),
316 ProfileFieldValue::Call(expected_call)
317 );
318 }
319}