1use js_int::UInt;
6use ruma_common::{OwnedMxcUri, OwnedUserId, presence::PresenceState};
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, Serialize, Deserialize)]
11#[allow(clippy::exhaustive_structs)]
12#[serde(tag = "type", rename = "m.presence")]
13pub struct PresenceEvent {
14 pub content: PresenceEventContent,
16
17 pub sender: OwnedUserId,
19}
20
21#[derive(Clone, Debug, Deserialize, Serialize)]
25#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
26pub struct PresenceEventContent {
27 #[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 #[serde(skip_serializing_if = "Option::is_none")]
40 pub currently_active: Option<bool>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub displayname: Option<String>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub last_active_ago: Option<UInt>,
49
50 pub presence: PresenceState,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub status_msg: Option<String>,
56}
57
58impl PresenceEventContent {
59 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}