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#[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/// The possible fields of a user's [profile].
28///
29/// [profile]: https://spec.matrix.org/v1.19/client-server-api/#profiles
30#[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    /// The user's avatar URL.
36    AvatarUrl,
37
38    /// The user's display name.
39    #[ruma_enum(rename = "displayname")]
40    DisplayName,
41
42    /// The user's time zone.
43    #[ruma_enum(rename = "m.tz")]
44    TimeZone,
45
46    /// The user's current status.
47    ///
48    /// This uses the unstable prefix defined in [MSC4426](https://github.com/matrix-org/matrix-spec-proposals/pull/4426).
49    #[cfg(feature = "unstable-msc4426")]
50    #[ruma_enum(rename = "org.matrix.msc4426.status")]
51    Status,
52
53    /// The user's call indicator.
54    ///
55    /// This uses the unstable prefix defined in [MSC4426](https://github.com/matrix-org/matrix-spec-proposals/pull/4426).
56    #[cfg(feature = "unstable-msc4426")]
57    #[ruma_enum(rename = "org.matrix.msc4426.call")]
58    Call,
59
60    #[doc(hidden)]
61    _Custom(PrivOwnedStr),
62}
63
64/// The possible values of a field of a user's [profile].
65///
66/// [profile]: https://spec.matrix.org/v1.19/client-server-api/#profiles
67#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
68#[serde(rename_all = "snake_case")]
69#[non_exhaustive]
70pub enum ProfileFieldValue {
71    /// The user's avatar URL.
72    AvatarUrl(OwnedMxcUri),
73
74    /// The user's display name.
75    #[serde(rename = "displayname")]
76    DisplayName(String),
77
78    /// The user's time zone.
79    #[serde(rename = "m.tz")]
80    TimeZone(String),
81
82    /// The user's current status.
83    ///
84    /// This uses the unstable prefix defined in [MSC4426](https://github.com/matrix-org/matrix-spec-proposals/pull/4426).
85    #[cfg(feature = "unstable-msc4426")]
86    #[serde(rename = "org.matrix.msc4426.status")]
87    Status(StatusProfileField),
88
89    /// The user's call indicator.
90    ///
91    /// This uses the unstable prefix defined in [MSC4426](https://github.com/matrix-org/matrix-spec-proposals/pull/4426).
92    #[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    /// Construct a new `ProfileFieldValue` with the given field and value.
103    ///
104    /// Prefer to use the public variants of `ProfileFieldValue` where possible; this constructor is
105    /// meant to be used for unsupported fields only and does not allow setting arbitrary data for
106    /// supported ones.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if the `field` is known and serialization of `value` to the corresponding
111    /// `ProfileFieldValue` variant fails.
112    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    /// The name of the field for this value.
122    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    /// Returns the value of the field.
136    ///
137    /// Prefer to use the public variants of `ProfileFieldValue` where possible; this method is
138    /// meant to be used for custom fields only.
139    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/// A text-only field describing the user’s current state, along with an emoji.
164///
165/// The emoji can be useful as a compact summary, or just for fun.
166#[cfg(feature = "unstable-msc4426")]
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[non_exhaustive]
169pub struct StatusProfileField {
170    /// The user’s chosen status text.
171    ///
172    /// Limited to 256 bytes. Does not support HTML.
173    pub text: String,
174
175    /// The user’s chosen status emoji.
176    ///
177    /// Limited to 32 bytes.
178    pub emoji: String,
179}
180
181#[cfg(feature = "unstable-msc4426")]
182impl StatusProfileField {
183    /// Creates a new `StatusProfileField` with the given text and emoji.
184    pub fn new(text: String, emoji: String) -> Self {
185        Self { text, emoji }
186    }
187}
188
189/// An indicator that the user is currently in a call, and optionally how long they’ve been in the
190/// call.
191#[cfg(feature = "unstable-msc4426")]
192#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[non_exhaustive]
194pub struct CallProfileField {
195    /// The time that the user joined the call.
196    ///
197    /// This allows users to see how long someone has been in a call.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub call_joined_ts: Option<SecondsSinceUnixEpoch>,
200}
201
202#[cfg(feature = "unstable-msc4426")]
203impl CallProfileField {
204    /// Creates a new `CallProfileField` with default values.
205    pub fn new() -> Self {
206        Self::default()
207    }
208}
209
210/// A custom value for a user's profile field.
211#[derive(Debug, Clone, PartialEq, Eq)]
212#[doc(hidden)]
213pub struct CustomProfileFieldValue {
214    /// The name of the field.
215    field: String,
216
217    /// The value of the field
218    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        // Avatar URL.
235        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        // Display name.
239        let value = ProfileFieldValue::DisplayName("Alice".to_owned());
240        assert_to_canonical_json_eq!(value, json!({ "displayname": "Alice" }));
241
242        // Custom field.
243        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        // Avatar URL.
250        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        // Display name.
257        let json = json!({ "displayname": "Alice" });
258        assert_eq!(
259            from_json_value::<ProfileFieldValue>(json).unwrap(),
260            ProfileFieldValue::DisplayName("Alice".to_owned())
261        );
262
263        // Custom field.
264        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        // Error if the object is empty.
270        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        // Status.
278        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        // Call.
286        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        // Status.
299        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        // Call.
310        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}