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-msc4532")]
6pub mod persistent;
7#[cfg(feature = "unstable-msc4495")]
8pub mod prompted;
9#[cfg(feature = "unstable-msc4495")]
10pub mod sharing;
11
12use js_int::UInt;
13#[cfg(feature = "unstable-msc4532")]
14use ruma_common::presence::PresenceStatus;
15use ruma_common::{OwnedMxcUri, OwnedUserId, presence::PresenceState};
16use serde::{Deserialize, Serialize};
17
18/// Presence event.
19#[derive(Clone, Debug, Serialize, Deserialize)]
20#[allow(clippy::exhaustive_structs)]
21#[serde(tag = "type", rename = "m.presence")]
22pub struct PresenceEvent {
23    /// Data specific to the event type.
24    pub content: PresenceEventContent,
25
26    /// Contains the fully-qualified ID of the user who sent this event.
27    pub sender: OwnedUserId,
28}
29
30/// Informs the room of members presence.
31///
32/// This is the only type a `PresenceEvent` can contain as its `content` field.
33#[derive(Clone, Debug)]
34#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
35pub struct PresenceEventContent {
36    /// The current avatar URL for this user.
37    ///
38    /// If you activate the `compat-empty-string-null` feature, this field being an empty string in
39    /// JSON will result in `None` here during deserialization.
40    pub avatar_url: Option<OwnedMxcUri>,
41
42    /// Whether the user is currently active or not.
43    ///
44    /// If the `unstable-msc4532` feature is enabled and `presence` is a MSC4532
45    /// presence state, this field will be ignored and will always have the same value
46    /// as [`PresenceState::currently_active`] after deserialization.
47    #[cfg_attr(
48        feature = "unstable-msc4532",
49        deprecated(
50            note = "Deprecated when MSC4532 is enabled, use `PresenceState::currently_active` instead"
51        )
52    )]
53    pub currently_active: Option<bool>,
54
55    /// The current display name for this user.
56    pub displayname: Option<String>,
57
58    /// The last time since this user performed some action, in milliseconds.
59    pub last_active_ago: Option<UInt>,
60
61    /// The presence state for this user.
62    pub presence: PresenceState,
63
64    /// An optional description to accompany the presence.
65    ///
66    /// If the `unstable-msc4532` feature is enabled, this field will always have the same value as
67    /// `status.msg` after deserialization.
68    #[cfg_attr(
69        feature = "unstable-msc4532",
70        deprecated(note = "Deprecated when MSC4532 is enabled, use `status` instead")
71    )]
72    pub status_msg: Option<String>,
73
74    /// Optional information to accompany the presence.
75    ///
76    /// This field uses the unstable prefix defined in [MSC4532].
77    ///
78    /// If this field is not present at deserialization, the value of `status_msg`
79    /// will be used instead.
80    ///
81    /// [MSC4532]: https://github.com/matrix-org/matrix-spec-proposals/pull/4532
82    #[cfg(feature = "unstable-msc4532")]
83    pub status: PresenceStatus,
84}
85
86/// The over-the-wire format for [`PresenceEventContent`]. This exists to enable a custom
87/// (de)serialization implementation providing backwards-compatibility for the `status_msg`
88/// field when [MSC4532] is enabled.
89///
90/// [MSC4532]: https://github.com/matrix-org/matrix-spec-proposals/pull/4532
91#[derive(Clone, Debug, Deserialize, Serialize)]
92#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
93struct PresenceEventRepr {
94    /// The current avatar URL for this user.
95    ///
96    /// If you activate the `compat-empty-string-null` feature, this field being an empty string in
97    /// JSON will result in `None` here during deserialization.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    #[cfg_attr(
100        feature = "compat-empty-string-null",
101        serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
102    )]
103    avatar_url: Option<OwnedMxcUri>,
104
105    /// Whether the user is currently active or not.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    currently_active: Option<bool>,
108
109    /// The current display name for this user.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    displayname: Option<String>,
112
113    /// The last time since this user performed some action, in milliseconds.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    last_active_ago: Option<UInt>,
116
117    /// The presence state for this user.
118    presence: PresenceState,
119
120    /// An optional description to accompany the presence.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    status_msg: Option<String>,
123
124    /// Optional information to accompany the presence.
125    ///
126    /// This field uses the unstable prefix defined in [MSC4532].
127    ///
128    /// If this field is not present at deserialization, the value of `status_msg`
129    /// will be used instead.
130    ///
131    /// [MSC4532]: https://github.com/matrix-org/matrix-spec-proposals/pull/4532
132    #[cfg(feature = "unstable-msc4532")]
133    #[serde(
134        skip_serializing_if = "ruma_common::serde::is_default",
135        rename = "org.continuwuity.presence_v2.msc4532.status",
136        default
137    )]
138    status: PresenceStatus,
139}
140
141impl<'de> Deserialize<'de> for PresenceEventContent {
142    #[allow(deprecated)]
143    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144    where
145        D: serde::Deserializer<'de>,
146    {
147        let PresenceEventRepr {
148            avatar_url,
149            currently_active,
150            displayname,
151            last_active_ago,
152            presence,
153            status_msg,
154            #[cfg(feature = "unstable-msc4532")]
155            status,
156        } = PresenceEventRepr::deserialize(deserializer)?;
157
158        #[cfg(feature = "unstable-msc4532")]
159        let status = if status == PresenceStatus::default() {
160            PresenceStatus::new(status_msg)
161        } else {
162            status
163        };
164
165        #[cfg(feature = "unstable-msc4532")]
166        let status_msg = status.msg.clone();
167
168        #[cfg(feature = "unstable-msc4532")]
169        let currently_active = if presence.is_msc4532_state() {
170            // ignore the provided currently_active value and use the proposal's definition
171            Some(presence.currently_active())
172        } else {
173            currently_active
174        };
175
176        Ok(Self {
177            avatar_url,
178            currently_active,
179            displayname,
180            last_active_ago,
181            presence,
182            #[cfg(feature = "unstable-msc4532")]
183            status,
184            status_msg,
185        })
186    }
187}
188
189impl Serialize for PresenceEventContent {
190    #[allow(deprecated)]
191    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
192    where
193        S: serde::Serializer,
194    {
195        let PresenceEventContent {
196            avatar_url,
197            currently_active,
198            displayname,
199            last_active_ago,
200            presence,
201            #[allow(unused_variables)]
202            status_msg,
203            #[cfg(feature = "unstable-msc4532")]
204            status,
205        } = self.clone();
206
207        PresenceEventRepr {
208            currently_active,
209            status_msg,
210            #[cfg(feature = "unstable-msc4532")]
211            status,
212            avatar_url,
213            last_active_ago,
214            displayname,
215            presence,
216        }
217        .serialize(serializer)
218    }
219}
220
221impl PresenceEventContent {
222    /// Creates a new `PresenceEventContent` with the given state.
223    pub fn new(presence: PresenceState) -> Self {
224        #[allow(deprecated)]
225        Self {
226            avatar_url: None,
227            currently_active: None,
228            displayname: None,
229            last_active_ago: None,
230            presence,
231            status_msg: None,
232            #[cfg(feature = "unstable-msc4532")]
233            status: PresenceStatus::default(),
234        }
235    }
236}
237
238#[cfg(test)]
239#[allow(deprecated)]
240mod tests {
241    use js_int::uint;
242    #[cfg(feature = "unstable-msc4532")]
243    use ruma_common::presence::PresenceStatus;
244    use ruma_common::{
245        canonical_json::assert_to_canonical_json_eq, owned_mxc_uri, presence::PresenceState,
246    };
247    use serde_json::{from_value as from_json_value, json};
248    use strass::assert_variant_eq;
249
250    use super::{PresenceEvent, PresenceEventContent};
251
252    #[test]
253    fn serialization() {
254        let content = PresenceEventContent {
255            avatar_url: Some(owned_mxc_uri!("mxc://localhost/wefuiwegh8742w")),
256            currently_active: Some(true),
257            displayname: None,
258            last_active_ago: Some(uint!(2_478_593)),
259            presence: PresenceState::Online,
260            #[allow(deprecated)]
261            status_msg: Some("Making cupcakes".into()),
262            #[cfg(feature = "unstable-msc4532")]
263            status: PresenceStatus::new(Some("Making cupcakes".into())),
264        };
265
266        #[cfg(feature = "unstable-msc4532")]
267        assert_to_canonical_json_eq!(
268            content,
269            json!({
270                "avatar_url": "mxc://localhost/wefuiwegh8742w",
271                "currently_active": true,
272                "last_active_ago": 2_478_593,
273                "presence": "online",
274                "org.continuwuity.presence_v2.msc4532.status": {
275                    "msg": "Making cupcakes"
276                },
277                "status_msg": "Making cupcakes"
278            }),
279        );
280        #[cfg(not(feature = "unstable-msc4532"))]
281        assert_to_canonical_json_eq!(
282            content,
283            json!({
284                "avatar_url": "mxc://localhost/wefuiwegh8742w",
285                "currently_active": true,
286                "last_active_ago": 2_478_593,
287                "presence": "online",
288                "status_msg": "Making cupcakes",
289            }),
290        );
291    }
292
293    #[test]
294    #[allow(deprecated)]
295    fn deserialization() {
296        let json = json!({
297            "content": {
298                "avatar_url": "mxc://localhost/wefuiwegh8742w",
299                "currently_active": true,
300                "last_active_ago": 2_478_593,
301                "presence": "online",
302                "status_msg": "Making cupcakes"
303            },
304            "sender": "@example:localhost",
305            "type": "m.presence"
306        });
307
308        let ev = from_json_value::<PresenceEvent>(json).unwrap();
309        assert_variant_eq!(ev.content.avatar_url, Some("mxc://localhost/wefuiwegh8742w"));
310        assert_eq!(ev.content.currently_active, Some(true));
311        assert_eq!(ev.content.displayname, None);
312        assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593)));
313        assert_eq!(ev.content.presence, PresenceState::Online);
314        assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes"));
315        #[cfg(feature = "unstable-msc4532")]
316        assert_eq!(ev.content.status.msg.as_deref(), Some("Making cupcakes"));
317        assert_eq!(ev.sender, "@example:localhost");
318
319        #[cfg(feature = "compat-empty-string-null")]
320        {
321            let json = json!({
322                "content": {
323                    "avatar_url": "",
324                    "currently_active": true,
325                    "last_active_ago": 2_478_593,
326                    "presence": "online",
327                    "status_msg": "Making cupcakes"
328                },
329                "sender": "@example:localhost",
330                "type": "m.presence"
331            });
332
333            let ev = from_json_value::<PresenceEvent>(json).unwrap();
334            assert_eq!(ev.content.avatar_url, None);
335            assert_eq!(ev.content.currently_active, Some(true));
336            assert_eq!(ev.content.displayname, None);
337            assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593)));
338            assert_eq!(ev.content.presence, PresenceState::Online);
339            assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes"));
340            #[cfg(feature = "unstable-msc4532")]
341            assert_eq!(ev.content.status.msg.as_deref(), Some("Making cupcakes"));
342            assert_eq!(ev.sender, "@example:localhost");
343        }
344    }
345}