tuwunel_api/client/
presence.rs1use std::time::Duration;
2
3use axum::extract::State;
4use ruma::{
5 api::client::presence::{get_presence, get_presence::v3::Response, set_presence},
6 events::presence::PresenceEvent,
7 presence::PresenceState,
8};
9use tuwunel_core::{Err, Result, err};
10
11use crate::Ruma;
12
13pub(crate) async fn set_presence_route(
17 State(services): State<crate::State>,
18 body: Ruma<set_presence::v3::Request>,
19) -> Result<set_presence::v3::Response> {
20 if !services.config.allow_local_presence {
21 return Err!(Request(Forbidden("Presence is disabled on this server")));
22 }
23
24 if body.sender_user() != body.user_id && body.appservice_info.is_none() {
25 return Err!(Request(InvalidParam("Not allowed to set presence of other users")));
26 }
27
28 services
29 .presence
30 .set_presence_for_device(
31 body.sender_user(),
32 body.sender_device.as_deref(),
33 &body.presence,
34 body.status_msg.clone(),
35 )
36 .await?;
37
38 Ok(set_presence::v3::Response {})
39}
40
41pub(crate) async fn get_presence_route(
46 State(services): State<crate::State>,
47 body: Ruma<get_presence::v3::Request>,
48) -> Result<Response> {
49 if !services.config.allow_local_presence {
50 return Err!(Request(Forbidden("Presence is disabled on this server",)));
51 }
52
53 let own_presence = body.sender_user() == body.user_id;
54
55 if !own_presence
56 && !services
57 .state_cache
58 .user_sees_user(body.sender_user(), &body.user_id)
59 .await
60 {
61 return Err!(Request(NotFound("Presence state for this user was not found")));
62 }
63
64 match services
65 .presence
66 .get_presence_optional(&body.user_id)
67 .await?
68 {
69 | Some(presence) => Ok(into_response(presence)),
70 | None => {
71 let known_local = !own_presence
72 && services.globals.user_is_local(&body.user_id)
73 && services.users.exists(&body.user_id).await;
74
75 may_default_presence(own_presence, known_local)
76 .then(default_presence)
77 .ok_or_else(|| {
78 err!(Request(NotFound("Presence state for this user was not found")))
79 })
80 },
81 }
82}
83
84fn into_response(presence: PresenceEvent) -> Response {
85 let content = presence.content;
86 let status_msg = content
87 .status_msg
88 .filter(|status| !status.is_empty());
89
90 let last_active_ago = content
91 .last_active_ago
92 .filter(|_| content.currently_active != Some(true))
93 .map(|millis| Duration::from_millis(millis.into()));
94
95 Response {
96 status_msg,
97 currently_active: content.currently_active,
98 last_active_ago,
99 presence: content.presence,
100 }
101}
102
103#[inline]
104fn may_default_presence(own_presence: bool, known_local: bool) -> bool {
105 own_presence || known_local
106}
107
108#[inline]
109fn default_presence() -> Response {
110 Response {
111 status_msg: None,
112 currently_active: None,
113 last_active_ago: None,
114 presence: PresenceState::Offline,
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use std::time::Duration;
121
122 use ruma::presence::PresenceState;
123 use serde_json::{from_value, json};
124
125 use super::{default_presence, into_response, may_default_presence};
126
127 #[test]
128 fn missing_presence_fallback_is_private() {
129 assert!(may_default_presence(true, false));
130 assert!(may_default_presence(false, true));
131 assert!(may_default_presence(true, true));
132 assert!(!may_default_presence(false, false));
133 }
134
135 #[test]
136 fn missing_presence_defaults_to_unobserved_offline() {
137 let response = default_presence();
138
139 assert_eq!(response.presence, PresenceState::Offline);
140 assert_eq!(response.currently_active, None);
141 assert_eq!(response.last_active_ago, None);
142 assert_eq!(response.status_msg, None);
143 }
144
145 #[test]
146 fn stored_presence_preserves_status_and_activity() {
147 let cases = [
148 (None, None, None, None, None),
149 (None, Some("busy"), Some(42), Some("busy"), Some(42)),
150 (Some(false), Some(""), Some(42), None, Some(42)),
151 (Some(true), Some("busy"), Some(42), Some("busy"), None),
152 ];
153
154 for (active, status, age, expected_status, expected_age) in cases {
155 let event = json!({
156 "sender": "@user:example.com",
157 "type": "m.presence",
158 "content": {
159 "presence": "online",
160 "currently_active": active,
161 "status_msg": status,
162 "last_active_ago": age,
163 },
164 });
165
166 let response = into_response(from_value(event).expect("valid presence event"));
167
168 assert_eq!(response.presence, PresenceState::Online);
169 assert_eq!(response.currently_active, active);
170 assert_eq!(response.status_msg.as_deref(), expected_status);
171 assert_eq!(response.last_active_ago, expected_age.map(Duration::from_millis));
172 }
173 }
174}