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, presence::PresenceState,
77    };
78    use serde_json::{from_value as from_json_value, json};
79
80    use super::{PresenceEvent, PresenceEventContent};
81
82    #[test]
83    fn serialization() {
84        let content = PresenceEventContent {
85            avatar_url: Some(mxc_uri!("mxc://localhost/wefuiwegh8742w").to_owned()),
86            currently_active: Some(false),
87            displayname: None,
88            last_active_ago: Some(uint!(2_478_593)),
89            presence: PresenceState::Online,
90            status_msg: Some("Making cupcakes".into()),
91        };
92
93        assert_to_canonical_json_eq!(
94            content,
95            json!({
96                "avatar_url": "mxc://localhost/wefuiwegh8742w",
97                "currently_active": false,
98                "last_active_ago": 2_478_593,
99                "presence": "online",
100                "status_msg": "Making cupcakes",
101            }),
102        );
103    }
104
105    #[test]
106    fn deserialization() {
107        let json = json!({
108            "content": {
109                "avatar_url": "mxc://localhost/wefuiwegh8742w",
110                "currently_active": false,
111                "last_active_ago": 2_478_593,
112                "presence": "online",
113                "status_msg": "Making cupcakes"
114            },
115            "sender": "@example:localhost",
116            "type": "m.presence"
117        });
118
119        let ev = from_json_value::<PresenceEvent>(json).unwrap();
120        assert_eq!(
121            ev.content.avatar_url.as_deref(),
122            Some(mxc_uri!("mxc://localhost/wefuiwegh8742w"))
123        );
124        assert_eq!(ev.content.currently_active, Some(false));
125        assert_eq!(ev.content.displayname, None);
126        assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593)));
127        assert_eq!(ev.content.presence, PresenceState::Online);
128        assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes"));
129        assert_eq!(ev.sender, "@example:localhost");
130
131        #[cfg(feature = "compat-empty-string-null")]
132        {
133            let json = json!({
134                "content": {
135                    "avatar_url": "",
136                    "currently_active": false,
137                    "last_active_ago": 2_478_593,
138                    "presence": "online",
139                    "status_msg": "Making cupcakes"
140                },
141                "sender": "@example:localhost",
142                "type": "m.presence"
143            });
144
145            let ev = from_json_value::<PresenceEvent>(json).unwrap();
146            assert_eq!(ev.content.avatar_url, None);
147            assert_eq!(ev.content.currently_active, Some(false));
148            assert_eq!(ev.content.displayname, None);
149            assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593)));
150            assert_eq!(ev.content.presence, PresenceState::Online);
151            assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes"));
152            assert_eq!(ev.sender, "@example:localhost");
153        }
154    }
155}