Skip to main content

ruma_common/profile/
user_profile.rs

1//! All the profile information for a user.
2
3use std::collections::{BTreeMap, btree_map};
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value as JsonValue;
7
8#[cfg(feature = "unstable-msc4262")]
9use super::UserProfileChanges;
10use super::{ProfileFieldName, ProfileFieldValue, static_profile_field::StaticProfileField};
11
12/// All the profile information for a user.
13#[derive(Clone, Debug, Default, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct UserProfile(BTreeMap<String, JsonValue>);
16
17impl UserProfile {
18    /// Creates a new empty `UserProfile`.
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Returns the value of the given profile field.
24    pub fn get(&self, field: &str) -> Option<&JsonValue> {
25        self.0.get(field)
26    }
27
28    /// Returns the value of the given [`StaticProfileField`].
29    ///
30    /// Returns `Ok(Some(_))` if the field is present and the value was deserialized
31    /// successfully, `Ok(None)` if the field is not set, or an error if deserialization of the
32    /// value failed.
33    pub fn get_static<F: StaticProfileField>(&self) -> Result<Option<F::Value>, serde_json::Error> {
34        self.0.get(F::NAME).map(|value| serde_json::from_value(value.clone())).transpose()
35    }
36
37    /// Gets an iterator over the fields of the profile.
38    pub fn iter(&self) -> btree_map::Iter<'_, String, JsonValue> {
39        self.0.iter()
40    }
41
42    /// Sets a field to the given value.
43    pub fn set(&mut self, field: String, value: JsonValue) {
44        self.0.insert(field, value);
45    }
46
47    /// Applies the changes from a [`UserProfileChanges`] to this profile.
48    #[cfg(feature = "unstable-msc4262")]
49    pub fn apply(&mut self, changes: UserProfileChanges) {
50        for (field, value) in changes.updated {
51            self.0.insert(field.to_string(), value);
52        }
53
54        for field in changes.removed {
55            self.0.remove(field.as_str());
56        }
57    }
58}
59
60impl FromIterator<(String, JsonValue)> for UserProfile {
61    fn from_iter<T: IntoIterator<Item = (String, JsonValue)>>(iter: T) -> Self {
62        Self(iter.into_iter().collect())
63    }
64}
65
66impl FromIterator<(ProfileFieldName, JsonValue)> for UserProfile {
67    fn from_iter<T: IntoIterator<Item = (ProfileFieldName, JsonValue)>>(iter: T) -> Self {
68        iter.into_iter().map(|(field, value)| (field.as_str().to_owned(), value)).collect()
69    }
70}
71
72impl FromIterator<ProfileFieldValue> for UserProfile {
73    fn from_iter<T: IntoIterator<Item = ProfileFieldValue>>(iter: T) -> Self {
74        iter.into_iter().map(|value| (value.field_name(), value.value().into_owned())).collect()
75    }
76}
77
78impl Extend<(String, JsonValue)> for UserProfile {
79    fn extend<T: IntoIterator<Item = (String, JsonValue)>>(&mut self, iter: T) {
80        self.0.extend(iter);
81    }
82}
83
84impl Extend<(ProfileFieldName, JsonValue)> for UserProfile {
85    fn extend<T: IntoIterator<Item = (ProfileFieldName, JsonValue)>>(&mut self, iter: T) {
86        self.extend(iter.into_iter().map(|(field, value)| (field.as_str().to_owned(), value)));
87    }
88}
89
90impl Extend<ProfileFieldValue> for UserProfile {
91    fn extend<T: IntoIterator<Item = ProfileFieldValue>>(&mut self, iter: T) {
92        self.extend(iter.into_iter().map(|value| (value.field_name(), value.value().into_owned())));
93    }
94}
95
96impl IntoIterator for UserProfile {
97    type Item = (String, JsonValue);
98    type IntoIter = btree_map::IntoIter<String, JsonValue>;
99
100    fn into_iter(self) -> Self::IntoIter {
101        self.0.into_iter()
102    }
103}
104
105#[cfg(test)]
106#[cfg(all(feature = "unstable-msc4262", feature = "unstable-msc4426"))]
107mod tests {
108    use std::collections::BTreeMap;
109
110    use serde_json::json;
111
112    use crate::{
113        owned_mxc_uri,
114        profile::{
115            AvatarUrl, Call, CallProfileField, DisplayName, ProfileFieldName, ProfileFieldValue,
116            Status, StatusProfileField, UserProfile, UserProfileChanges,
117        },
118    };
119
120    #[test]
121    fn apply_profile_update() {
122        let mut profile = UserProfile::from_iter([
123            ProfileFieldValue::DisplayName("Alice".to_owned()),
124            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
125            ProfileFieldValue::Status(StatusProfileField {
126                text: "Working".to_owned(),
127                emoji: "🧑‍💻".to_owned(),
128            }),
129        ]);
130
131        let mut profile_update = UserProfileChanges::new();
132        profile_update.removed = vec![ProfileFieldName::AvatarUrl];
133        profile_update.updated = BTreeMap::from([
134            (ProfileFieldName::Status, json!({ "text": "Holiday", "emoji": "🏖️"})),
135            (ProfileFieldName::Call, json!({})),
136        ]);
137
138        profile.apply(profile_update);
139
140        assert_eq!(
141            profile.get_static::<DisplayName>().unwrap().unwrap(),
142            "Alice".to_owned(),
143            "The display name should be preserved."
144        );
145        assert!(
146            profile.get_static::<AvatarUrl>().unwrap().is_none(),
147            "The avatar should be removed."
148        );
149        assert_eq!(
150            profile.get_static::<Status>().unwrap().unwrap(),
151            StatusProfileField { text: "Holiday".to_owned(), emoji: "🏖️".to_owned() },
152            "The status should be updated."
153        );
154        assert_eq!(
155            profile.get_static::<Call>().unwrap().unwrap(),
156            CallProfileField { call_joined_ts: None },
157            "The call indicator should be set."
158        );
159    }
160}