1#[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#[derive(Clone, Debug, Serialize, Deserialize)]
20#[allow(clippy::exhaustive_structs)]
21#[serde(tag = "type", rename = "m.presence")]
22pub struct PresenceEvent {
23 pub content: PresenceEventContent,
25
26 pub sender: OwnedUserId,
28}
29
30#[derive(Clone, Debug)]
34#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
35pub struct PresenceEventContent {
36 pub avatar_url: Option<OwnedMxcUri>,
41
42 #[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 pub displayname: Option<String>,
57
58 pub last_active_ago: Option<UInt>,
60
61 pub presence: PresenceState,
63
64 #[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 #[cfg(feature = "unstable-msc4532")]
83 pub status: PresenceStatus,
84}
85
86#[derive(Clone, Debug, Deserialize, Serialize)]
92#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
93struct PresenceEventRepr {
94 #[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 #[serde(skip_serializing_if = "Option::is_none")]
107 currently_active: Option<bool>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
111 displayname: Option<String>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
115 last_active_ago: Option<UInt>,
116
117 presence: PresenceState,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
122 status_msg: Option<String>,
123
124 #[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 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 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}