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, mxc_uri, owned_mxc_uri,
82        presence::PresenceState,
83    };
84    use serde_json::{from_value as from_json_value, json};
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_eq!(
127            ev.content.avatar_url.as_deref(),
128            Some(mxc_uri!("mxc://localhost/wefuiwegh8742w"))
129        );
130        assert_eq!(ev.content.currently_active, Some(false));
131        assert_eq!(ev.content.displayname, None);
132        assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593)));
133        assert_eq!(ev.content.presence, PresenceState::Online);
134        assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes"));
135        assert_eq!(ev.sender, "@example:localhost");
136
137        #[cfg(feature = "compat-empty-string-null")]
138        {
139            let json = json!({
140                "content": {
141                    "avatar_url": "",
142                    "currently_active": false,
143                    "last_active_ago": 2_478_593,
144                    "presence": "online",
145                    "status_msg": "Making cupcakes"
146                },
147                "sender": "@example:localhost",
148                "type": "m.presence"
149            });
150
151            let ev = from_json_value::<PresenceEvent>(json).unwrap();
152            assert_eq!(ev.content.avatar_url, None);
153            assert_eq!(ev.content.currently_active, Some(false));
154            assert_eq!(ev.content.displayname, None);
155            assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593)));
156            assert_eq!(ev.content.presence, PresenceState::Online);
157            assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes"));
158            assert_eq!(ev.sender, "@example:localhost");
159        }
160    }
161}