Skip to main content

ruma_events/
presence.rs

1//! A presence event is represented by a struct with a set content field.
2//!
3//! The only content valid for this event is `PresenceEventContent`.
4
5#[cfg(feature = "unstable-msc4495")]
6pub mod prompted;
7#[cfg(feature = "unstable-msc4495")]
8pub mod sharing;
9
10use js_int::UInt;
11use ruma_common::{OwnedMxcUri, OwnedUserId, presence::PresenceState};
12use serde::{Deserialize, Serialize};
13
14/// Presence event.
15#[derive(Clone, Debug, Serialize, Deserialize)]
16#[allow(clippy::exhaustive_structs)]
17#[serde(tag = "type", rename = "m.presence")]
18pub struct PresenceEvent {
19    /// Data specific to the event type.
20    pub content: PresenceEventContent,
21
22    /// Contains the fully-qualified ID of the user who sent this event.
23    pub sender: OwnedUserId,
24}
25
26/// Informs the room of members presence.
27///
28/// This is the only type a `PresenceEvent` can contain as its `content` field.
29#[derive(Clone, Debug, Deserialize, Serialize)]
30#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
31pub struct PresenceEventContent {
32    /// The current avatar URL for this user.
33    ///
34    /// If you activate the `compat-empty-string-null` feature, this field being an empty string in
35    /// JSON will result in `None` here during deserialization.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    #[cfg_attr(
38        feature = "compat-empty-string-null",
39        serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
40    )]
41    pub avatar_url: Option<OwnedMxcUri>,
42
43    /// Whether or not the user is currently active.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub currently_active: Option<bool>,
46
47    /// The current display name for this user.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub displayname: Option<String>,
50
51    /// The last time since this user performed some action, in milliseconds.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub last_active_ago: Option<UInt>,
54
55    /// The presence state for this user.
56    pub presence: PresenceState,
57
58    /// An optional description to accompany the presence.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub status_msg: Option<String>,
61}
62
63impl PresenceEventContent {
64    /// Creates a new `PresenceEventContent` with the given state.
65    pub fn new(presence: PresenceState) -> Self {
66        Self {
67            avatar_url: None,
68            currently_active: None,
69            displayname: None,
70            last_active_ago: None,
71            presence,
72            status_msg: None,
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use js_int::uint;
80    use ruma_common::{
81        canonical_json::assert_to_canonical_json_eq, owned_mxc_uri, presence::PresenceState,
82    };
83    use serde_json::{from_value as from_json_value, json};
84    use strass::assert_variant_eq;
85
86    use super::{PresenceEvent, PresenceEventContent};
87
88    #[test]
89    fn serialization() {
90        let content = PresenceEventContent {
91            avatar_url: Some(owned_mxc_uri!("mxc://localhost/wefuiwegh8742w")),
92            currently_active: Some(false),
93            displayname: None,
94            last_active_ago: Some(uint!(2_478_593)),
95            presence: PresenceState::Online,
96            status_msg: Some("Making cupcakes".into()),
97        };
98
99        assert_to_canonical_json_eq!(
100            content,
101            json!({
102                "avatar_url": "mxc://localhost/wefuiwegh8742w",
103                "currently_active": false,
104                "last_active_ago": 2_478_593,
105                "presence": "online",
106                "status_msg": "Making cupcakes",
107            }),
108        );
109    }
110
111    #[test]
112    fn deserialization() {
113        let json = json!({
114            "content": {
115                "avatar_url": "mxc://localhost/wefuiwegh8742w",
116                "currently_active": false,
117                "last_active_ago": 2_478_593,
118                "presence": "online",
119                "status_msg": "Making cupcakes"
120            },
121            "sender": "@example:localhost",
122            "type": "m.presence"
123        });
124
125        let ev = from_json_value::<PresenceEvent>(json).unwrap();
126        assert_variant_eq!(ev.content.avatar_url, Some("mxc://localhost/wefuiwegh8742w"));
127        assert_eq!(ev.content.currently_active, Some(false));
128        assert_eq!(ev.content.displayname, None);
129        assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593)));
130        assert_eq!(ev.content.presence, PresenceState::Online);
131        assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes"));
132        assert_eq!(ev.sender, "@example:localhost");
133
134        #[cfg(feature = "compat-empty-string-null")]
135        {
136            let json = json!({
137                "content": {
138                    "avatar_url": "",
139                    "currently_active": false,
140                    "last_active_ago": 2_478_593,
141                    "presence": "online",
142                    "status_msg": "Making cupcakes"
143                },
144                "sender": "@example:localhost",
145                "type": "m.presence"
146            });
147
148            let ev = from_json_value::<PresenceEvent>(json).unwrap();
149            assert_eq!(ev.content.avatar_url, None);
150            assert_eq!(ev.content.currently_active, Some(false));
151            assert_eq!(ev.content.displayname, None);
152            assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593)));
153            assert_eq!(ev.content.presence, PresenceState::Online);
154            assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes"));
155            assert_eq!(ev.sender, "@example:localhost");
156        }
157    }
158}