Skip to main content

ruma_common/
profile.rs

1//! Common types for user profile endpoints.
2
3use 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/// The possible fields of a user's [profile].
24///
25/// [profile]: https://spec.matrix.org/v1.18/client-server-api/#profiles
26#[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    /// The user's avatar URL.
32    AvatarUrl,
33
34    /// The user's display name.
35    #[ruma_enum(rename = "displayname")]
36    DisplayName,
37
38    /// The user's time zone.
39    #[ruma_enum(rename = "m.tz")]
40    TimeZone,
41
42    /// The user's current status.
43    ///
44    /// This uses the unstable prefix defined in [MSC4426](https://github.com/matrix-org/matrix-spec-proposals/pull/4426).
45    #[cfg(feature = "unstable-msc4426")]
46    #[ruma_enum(rename = "org.matrix.msc4426.status")]
47    Status,
48
49    /// The user's call indicator.
50    ///
51    /// This uses the unstable prefix defined in [MSC4426](https://github.com/matrix-org/matrix-spec-proposals/pull/4426).
52    #[cfg(feature = "unstable-msc4426")]
53    #[ruma_enum(rename = "org.matrix.msc4426.call")]
54    Call,
55
56    #[doc(hidden)]
57    _Custom(PrivOwnedStr),
58}
59
60/// The possible values of a field of a user's [profile].
61///
62/// [profile]: https://spec.matrix.org/v1.18/client-server-api/#profiles
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64#[serde(rename_all = "snake_case")]
65#[non_exhaustive]
66pub enum ProfileFieldValue {
67    /// The user's avatar URL.
68    AvatarUrl(OwnedMxcUri),
69
70    /// The user's display name.
71    #[serde(rename = "displayname")]
72    DisplayName(String),
73
74    /// The user's time zone.
75    #[serde(rename = "m.tz")]
76    TimeZone(String),
77
78    /// The user's current status.
79    ///
80    /// This uses the unstable prefix defined in [MSC4426](https://github.com/matrix-org/matrix-spec-proposals/pull/4426).
81    #[cfg(feature = "unstable-msc4426")]
82    #[serde(rename = "org.matrix.msc4426.status")]
83    Status(StatusProfileField),
84
85    /// The user's call indicator.
86    ///
87    /// This uses the unstable prefix defined in [MSC4426](https://github.com/matrix-org/matrix-spec-proposals/pull/4426).
88    #[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    /// Construct a new `ProfileFieldValue` with the given field and value.
99    ///
100    /// Prefer to use the public variants of `ProfileFieldValue` where possible; this constructor is
101    /// meant to be used for unsupported fields only and does not allow setting arbitrary data for
102    /// supported ones.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the `field` is known and serialization of `value` to the corresponding
107    /// `ProfileFieldValue` variant fails.
108    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    /// The name of the field for this value.
118    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    /// Returns the value of the field.
132    ///
133    /// Prefer to use the public variants of `ProfileFieldValue` where possible; this method is
134    /// meant to be used for custom fields only.
135    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/// A text-only field describing the user’s current state, along with an emoji.
160///
161/// The emoji can be useful as a compact summary, or just for fun.
162#[cfg(feature = "unstable-msc4426")]
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[non_exhaustive]
165pub struct StatusProfileField {
166    /// The user’s chosen status text.
167    ///
168    /// Limited to 256 bytes. Does not support HTML.
169    pub text: String,
170
171    /// The user’s chosen status emoji.
172    ///
173    /// Limited to 32 bytes.
174    pub emoji: String,
175}
176
177/// An indicator that the user is currently in a call, and optionally how long they’ve been in the
178/// call.
179#[cfg(feature = "unstable-msc4426")]
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[non_exhaustive]
182pub struct CallProfileField {
183    /// The time that the user joined the call.
184    ///
185    /// This allows users to see how long someone has been in a call.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub call_joined_ts: Option<SecondsSinceUnixEpoch>,
188}
189
190/// A custom value for a user's profile field.
191#[derive(Debug, Clone, PartialEq, Eq)]
192#[doc(hidden)]
193pub struct CustomProfileFieldValue {
194    /// The name of the field.
195    field: String,
196
197    /// The value of the field
198    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        // Avatar URL.
215        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        // Display name.
219        let value = ProfileFieldValue::DisplayName("Alice".to_owned());
220        assert_to_canonical_json_eq!(value, json!({ "displayname": "Alice" }));
221
222        // Custom field.
223        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        // Avatar URL.
230        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        // Display name.
237        let json = json!({ "displayname": "Alice" });
238        assert_eq!(
239            from_json_value::<ProfileFieldValue>(json).unwrap(),
240            ProfileFieldValue::DisplayName("Alice".to_owned())
241        );
242
243        // Custom field.
244        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        // Error if the object is empty.
250        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        // Status.
258        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        // Call.
268        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        // Status.
281        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        // Call.
292        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}