1use std::time::Duration;
8
9use futures::TryFutureExt;
10use ruma::{
11 DeviceId, OwnedUserId, UInt, UserId, events::presence::PresenceEvent, presence::PresenceState,
12};
13use tokio::time::sleep;
14use tuwunel_core::{
15 Error, Result, debug,
16 debug::INFO_SPAN_LEVEL,
17 error,
18 result::LogErr,
19 trace,
20 utils::{future::OptionFutureExt, option::OptionExt},
21};
22
23use super::{
24 Ping, Service, TimerFired,
25 aggregate::{self, StatusMsg},
26};
27
28impl Service {
29 fn device_key(device_id: Option<&DeviceId>, is_remote: bool) -> aggregate::DeviceKey {
30 if is_remote {
31 return aggregate::DeviceKey::Remote;
32 }
33
34 match device_id {
35 | Some(device_id) => aggregate::DeviceKey::Device(device_id.to_owned()),
36 | None => aggregate::DeviceKey::UnknownLocal,
37 }
38 }
39
40 fn schedule_presence_timer(
41 &self,
42 user_id: &UserId,
43 presence_state: &PresenceState,
44 count: u64,
45 ) -> Result {
46 if !(self.timeout_remote_users || self.services.globals.user_is_local(user_id))
47 || user_id == self.services.globals.server_user
48 {
49 return Ok(());
50 }
51
52 let timeout = match presence_state {
53 | PresenceState::Online =>
54 self.services
55 .server
56 .config
57 .presence_idle_timeout_s,
58 | _ =>
59 self.services
60 .server
61 .config
62 .presence_offline_timeout_s,
63 };
64
65 self.timer_channel
66 .0
67 .send((user_id.to_owned(), Duration::from_secs(timeout), count))
68 .map_err(|e| {
69 error!("Failed to add presence timer: {}", e);
70 Error::bad_database("Failed to add presence timer")
71 })
72 }
73
74 fn refresh_skip_decision(
75 refresh_window_ms: Option<u64>,
76 last_event: Option<&PresenceEvent>,
77 last_count: Option<u64>,
78 ) -> Option<(u64, u64)> {
79 let (Some(refresh_ms), Some(event), Some(count)) =
80 (refresh_window_ms, last_event, last_count)
81 else {
82 return None;
83 };
84
85 let last_last_active_ago: u64 = event.content.last_active_ago?.into();
86
87 (last_last_active_ago < refresh_ms).then_some((count, last_last_active_ago))
88 }
89
90 fn timer_is_stale(expected_count: u64, current_count: u64) -> bool {
91 expected_count != current_count
92 }
93
94 #[tracing::instrument(
95 name = "presence",
96 level = INFO_SPAN_LEVEL,
97 skip_all,
98 fields(
99 %user_id,
100 ?device_key,
101 %state,
102 ?currently_active,
103 ),
104 )]
105 #[expect(clippy::too_many_arguments)]
106 async fn apply_device_presence_update(
107 &self,
108 user_id: &UserId,
109 device_key: aggregate::DeviceKey,
110 state: &PresenceState,
111 currently_active: Option<bool>,
112 last_active_ago: Option<UInt>,
113 status_msg: StatusMsg,
114 refresh_window_ms: Option<u64>,
115 ) -> Result {
116 let now = tuwunel_core::utils::millis_since_unix_epoch();
117 let preserve_status = matches!(status_msg, StatusMsg::Unchanged);
118
119 debug!(
121 ?user_id,
122 ?device_key,
123 ?state,
124 currently_active,
125 last_active_ago = last_active_ago.map(u64::from),
126 "Presence update received"
127 );
128
129 self.device_presence
130 .update(
131 user_id,
132 device_key,
133 state,
134 currently_active,
135 last_active_ago,
136 status_msg,
137 now,
138 )
139 .await;
140
141 let aggregated = self
143 .device_presence
144 .aggregate(user_id, now, self.idle_timeout, self.offline_timeout)
145 .await;
146
147 debug!(
148 ?user_id,
149 agg_state = ?aggregated.state,
150 agg_currently_active = aggregated.currently_active,
151 agg_last_active_ts = aggregated.last_active_ts,
152 agg_device_count = aggregated.device_count,
153 "Presence aggregate computed"
154 );
155
156 let last_presence = self.db.get_presence(user_id).await;
158 let (last_count, last_event) = match last_presence {
159 | Ok((count, event)) => (Some(count), Some(event)),
160 | Err(_) => (None, None),
161 };
162
163 let last_state = last_event
164 .as_ref()
165 .map(|event| event.content.presence.clone());
166
167 let state_changed = match &last_event {
168 | Some(event) => event.content.presence != aggregated.state,
169 | None => true,
170 };
171
172 if !state_changed
174 && let Some((count, last_last_active_ago)) =
175 Self::refresh_skip_decision(refresh_window_ms, last_event.as_ref(), last_count)
176 {
177 let presence = last_event
178 .as_ref()
179 .map(|event| &event.content.presence)
180 .unwrap_or(state);
181
182 self.schedule_presence_timer(user_id, presence, count)
183 .log_err()
184 .ok();
185
186 debug!(
187 ?user_id,
188 ?state,
189 last_last_active_ago,
190 "Skipping presence update: refresh window (timer rescheduled)"
191 );
192
193 return Ok(());
194 }
195
196 if matches!(last_state, Some(PresenceState::Online))
198 && aggregated.state != PresenceState::Online
199 {
200 debug!(
201 ?user_id,
202 from = ?PresenceState::Online,
203 to = ?aggregated.state,
204 "Presence went inactive; flushing suppressed pushes"
205 );
206
207 self.services
208 .sending
209 .schedule_flush_suppressed_for_user(
210 user_id.to_owned(),
211 "presence->inactive (aggregate)",
212 );
213 }
214
215 let fallback_status = || {
217 last_event
218 .and_then(|event| event.content.status_msg)
219 .filter(|msg| !msg.is_empty())
220 };
221
222 let status_msg = aggregated
223 .status_msg
224 .or_else(|| preserve_status.then(fallback_status).flatten());
225
226 let last_active_ago =
227 Some(UInt::new_saturating(now.saturating_sub(aggregated.last_active_ts)));
228
229 self.set_presence(
230 user_id,
231 &aggregated.state,
232 Some(aggregated.currently_active),
233 last_active_ago,
234 status_msg,
235 )
236 .await
237 }
238
239 pub async fn maybe_ping_presence(&self, user_id: &UserId, args: Ping<'_>) -> Result {
246 const REFRESH_TIMEOUT: u64 = 30 * 1000;
247
248 if args.appservice.is_some()
249 || !self.services.server.config.allow_local_presence
250 || self.services.db.is_read_only()
251 {
252 return Ok(());
253 }
254
255 let update_device_seen = args.device_id.map_async(|device_id| {
256 self.services
257 .users
258 .update_device_last_seen(user_id, device_id, args.client_ip, None)
259 });
260
261 let new_state = args.new_state.unwrap_or(&PresenceState::Online);
262 let currently_active = *new_state == PresenceState::Online;
263 let set_presence = self.apply_device_presence_update(
264 user_id,
265 Self::device_key(args.device_id, false),
266 new_state,
267 Some(currently_active),
268 UInt::new(0),
269 StatusMsg::Unchanged,
270 Some(REFRESH_TIMEOUT),
271 );
272
273 debug!(?user_id, ?new_state, currently_active, "Presence ping accepted");
274
275 futures::future::try_join(set_presence, update_device_seen.unwrap_or(Ok(())))
276 .map_ok(|_| ())
277 .await
278 }
279
280 pub async fn set_presence_for_device(
282 &self,
283 user_id: &UserId,
284 device_id: Option<&DeviceId>,
285 state: &PresenceState,
286 status_msg: Option<String>,
287 ) -> Result {
288 let currently_active = *state == PresenceState::Online;
289 self.apply_device_presence_update(
290 user_id,
291 Self::device_key(device_id, false),
292 state,
293 Some(currently_active),
294 None,
295 StatusMsg::Set(status_msg),
296 None,
297 )
298 .await
299 }
300
301 pub async fn set_presence_from_federation(
303 &self,
304 user_id: &UserId,
305 state: &PresenceState,
306 currently_active: bool,
307 last_active_ago: UInt,
308 status_msg: Option<String>,
309 ) -> Result {
310 self.apply_device_presence_update(
311 user_id,
312 Self::device_key(None, true),
313 state,
314 Some(currently_active),
315 Some(last_active_ago),
316 StatusMsg::Set(status_msg),
317 None,
318 )
319 .await
320 }
321
322 pub async fn set_presence(
324 &self,
325 user_id: &UserId,
326 state: &PresenceState,
327 currently_active: Option<bool>,
328 last_active_ago: Option<UInt>,
329 status_msg: Option<String>,
330 ) -> Result {
331 let presence_state = match state.as_str() {
332 | "" => &PresenceState::Offline, | &_ => state,
334 };
335
336 let count = self
337 .db
338 .set_presence(user_id, presence_state, currently_active, last_active_ago, status_msg)
339 .await?;
340
341 if let Some(count) = count {
342 let is_local = self.services.globals.user_is_local(user_id);
343 let is_server_user = user_id == self.services.globals.server_user;
344 let allow_timeout = self.timeout_remote_users || is_local;
345
346 if allow_timeout && !is_server_user {
347 self.schedule_presence_timer(user_id, presence_state, count)?;
348 }
349 }
350
351 Ok(())
352 }
353
354 pub(super) async fn process_presence_timer(
355 &self,
356 user_id: &OwnedUserId,
357 expected_count: u64,
358 ) -> Result {
359 let Ok((current_count, presence)) = self.db.get_presence_raw(user_id).await else {
360 return Ok(());
361 };
362
363 if Self::timer_is_stale(expected_count, current_count) {
364 trace!(?user_id, expected_count, current_count, "Skipping stale presence timer");
365 return Ok(());
366 }
367
368 let presence_state = presence.state.clone();
369 let now = tuwunel_core::utils::millis_since_unix_epoch();
370 let aggregated = self
371 .device_presence
372 .aggregate(user_id, now, self.idle_timeout, self.offline_timeout)
373 .await;
374
375 if aggregated.device_count == 0 {
376 let last_active_ago =
377 Some(UInt::new_saturating(now.saturating_sub(presence.last_active_ts)));
378 let status_msg = presence.status_msg;
379
380 let new_state = match (&presence_state, last_active_ago.map(u64::from)) {
381 | (PresenceState::Online, Some(ago)) if ago >= self.idle_timeout =>
382 Some(PresenceState::Unavailable),
383 | (PresenceState::Unavailable, Some(ago)) if ago >= self.offline_timeout =>
384 Some(PresenceState::Offline),
385 | _ => None,
386 };
387
388 debug!(
389 "Processed presence timer for user '{user_id}': Old state = {presence_state}, \
390 New state = {new_state:?}"
391 );
392
393 if let Some(new_state) = new_state {
394 if matches!(new_state, PresenceState::Unavailable | PresenceState::Offline) {
395 self.services
396 .sending
397 .schedule_flush_suppressed_for_user(
398 user_id.to_owned(),
399 "presence->inactive",
400 );
401 }
402 self.set_presence(user_id, &new_state, Some(false), last_active_ago, status_msg)
403 .await?;
404 }
405
406 return Ok(());
407 }
408
409 if aggregated.state == presence_state {
410 self.schedule_presence_timer(user_id, &presence_state, current_count)
411 .log_err()
412 .ok();
413 return Ok(());
414 }
415
416 if matches!(aggregated.state, PresenceState::Unavailable | PresenceState::Offline) {
417 self.services
418 .sending
419 .schedule_flush_suppressed_for_user(user_id.to_owned(), "presence->inactive");
420 }
421
422 let status_msg = aggregated.status_msg.or(presence.status_msg);
423 let last_active_ago =
424 Some(UInt::new_saturating(now.saturating_sub(aggregated.last_active_ts)));
425
426 self.set_presence(
427 user_id,
428 &aggregated.state,
429 Some(aggregated.currently_active),
430 last_active_ago,
431 status_msg,
432 )
433 .await?;
434
435 Ok(())
436 }
437}
438
439pub(super) async fn presence_timer(
440 user_id: OwnedUserId,
441 timeout: Duration,
442 count: u64,
443) -> TimerFired {
444 sleep(timeout).await;
445
446 (user_id, count)
447}
448
449#[cfg(test)]
450mod tests {
451 use ruma::{uint, user_id};
452
453 use super::*;
454
455 #[test]
456 fn refresh_window_skip_decision() {
457 let user_id = user_id!("@alice:example.com");
458 let event = PresenceEvent {
459 sender: user_id.to_owned(),
460 content: ruma::events::presence::PresenceEventContent {
461 presence: PresenceState::Online,
462 status_msg: None,
463 currently_active: Some(true),
464 last_active_ago: Some(uint!(10)),
465 avatar_url: None,
466 displayname: None,
467 },
468 };
469
470 let decision = Service::refresh_skip_decision(Some(20), Some(&event), Some(5));
471 assert_eq!(decision, Some((5, 10)));
472
473 let decision = Service::refresh_skip_decision(Some(5), Some(&event), Some(5));
474 assert_eq!(decision, None);
475
476 let event_missing_ago = PresenceEvent {
477 sender: user_id.to_owned(),
478 content: ruma::events::presence::PresenceEventContent {
479 presence: PresenceState::Online,
480 status_msg: None,
481 currently_active: Some(true),
482 last_active_ago: None,
483 avatar_url: None,
484 displayname: None,
485 },
486 };
487
488 let decision =
489 Service::refresh_skip_decision(Some(20), Some(&event_missing_ago), Some(5));
490 assert_eq!(decision, None);
491
492 let decision = Service::refresh_skip_decision(Some(20), None, Some(5));
493 assert_eq!(decision, None);
494 }
495
496 #[test]
497 fn timer_stale_detection() {
498 assert!(Service::timer_is_stale(2, 3));
499 assert!(!Service::timer_is_stale(2, 2));
500 }
501}