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