Skip to main content

tuwunel_api/client/
presence.rs

1use 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
13/// # `PUT /_matrix/client/r0/presence/{userId}/status`
14///
15/// Sets the presence state of the sender user.
16pub(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
41/// Gets the presence state of the given user.
42///
43/// Access requires the user's own identity or a shared room. Missing state
44/// defaults to offline for self or a known local user after authorization.
45pub(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}