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