Skip to main content

arminius_server_client/handlers/
presence.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Crate containing routes for presence
6
7#![allow(
8	clippy::allow_attributes,
9	reason = "This source file contains enums which allow `clippy::missing_docs_in_private_items`. But an exception to this lint will mistakenly report that it's unfulfilled when used with expect, even though it is not."
10)]
11
12mod tests;
13
14use std::time::Duration;
15
16use arminius_lib_component::{
17	Component, ServerState,
18	error::{InternalServerError, MatrixApiError, ResultExt as _},
19	ruma_requests::ArminiusResponse,
20};
21use arminius_lib_db::DefaultDb;
22use arminius_lib_db_presence::PresenceQueries as _;
23use axum::{
24	Router,
25	extract::State,
26	http::StatusCode,
27	routing::{get, put},
28};
29use ruma::{
30	api::{
31		client::presence::{get_presence, set_presence},
32		error::ErrorKind,
33	},
34	presence::PresenceState,
35};
36
37use crate::{
38	auth::{AccountActionNotAuthorized, check_user_matches},
39	client_request::AuthenticatedClientRequest,
40};
41
42// NOTE: These two should be mirrored in the migrations
43/// The maximum length of a status
44const MAX_STATUS_LEN: usize = 255;
45/// The maximum permitted length of a presence state
46const MAX_PRESENCE_LEN: usize = 100;
47
48/// Handler for `GET /_matrix/client/v3/presence/{userId}/status` (See
49/// <https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3presenceuseridstatus>).
50///
51/// This endpoint is used to manually fetch presence of other users.
52async fn get_presence_handler(
53	mut database: State<DefaultDb>,
54	AuthenticatedClientRequest(_, request): AuthenticatedClientRequest<get_presence::v3::Request>,
55) -> Result<ArminiusResponse<get_presence::v3::Response>, InternalServerError> {
56	// TODO: Check that users share a room before they're allowed to check each
57	// other's presence. Issue #365
58	match database
59		.fetch_presence_row(request.user_id.as_str())
60		.await
61		.unrecoverable_error("getting presence for user")?
62	{
63		Some(presence) => {
64			// Since we only allow manual updating of presence, no presence set
65			// implies Offline. This is also the default trait, but we
66			// specify it ourselves just to be sure
67			let mut response = get_presence::v3::Response::new(
68				presence.manual_presence.map_or(PresenceState::Offline, PresenceState::from),
69			);
70			if response.presence == PresenceState::Online {
71				response.currently_active = Some(true);
72			} else {
73				response.last_active_ago = presence
74					.last_active
75					.map(u64::try_from)
76					.map(Result::unwrap_or_default)
77					.map(Duration::from_millis);
78			}
79			response.status_msg = presence.status;
80
81			Ok(response.into())
82		}
83		None => {
84			let response = get_presence::v3::Response::new(PresenceState::Offline);
85			Ok(response.into())
86		}
87	}
88}
89
90#[allow(
91	missing_docs,
92	clippy::missing_docs_in_private_items,
93	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
94)]
95#[derive(thiserror::Error, Debug, MatrixApiError)]
96enum SetPresenceError {
97	/// Unrecoverable error.
98	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
99	#[error(transparent)]
100	InternalServerError(#[from] InternalServerError),
101	/// Error for when presence is too long
102	#[error("Presence must be no longer than {MAX_PRESENCE_LEN} characters")]
103	#[matrix_api_error(status_code = StatusCode::PAYLOAD_TOO_LARGE, error_kind = ErrorKind::TooLarge)]
104	PresenceTooLong,
105	/// Error for when Status is too long
106	#[error("Status must be no longer than {MAX_STATUS_LEN} characters")]
107	#[matrix_api_error(status_code = StatusCode::PAYLOAD_TOO_LARGE, error_kind = ErrorKind::TooLarge)]
108	StatusTooLong,
109	#[error(transparent)]
110	AccountActionNotAuthorized(#[from] AccountActionNotAuthorized),
111}
112
113/// Handler for `PUT /_matrix/client/v3/presence/{userId}/status` (See
114/// <https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3presenceuseridstatus>).
115///
116/// This endpoint is used to manually update the presence and status.
117///
118/// We don't validate that the presence string is one of the options outlined by
119/// the spec. Instead, we just let the user set any string. This is easier, and
120/// is the same behaviour as Conduit. Dendrite simply has an "unknown" state,
121/// which they use for all `presence` values that are neither `online`,
122/// `unavailable` or `offline`. Synapse allows `org.matrix.msc3026.busy`.
123async fn set_presence_handler(
124	mut database: State<DefaultDb>,
125	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
126		set_presence::v3::Request,
127	>,
128) -> Result<ArminiusResponse<set_presence::v3::Response>, SetPresenceError> {
129	check_user_matches(&request.user_id, &claims.user_id)?;
130
131	if request.presence.as_str().len() > MAX_PRESENCE_LEN {
132		return Err(SetPresenceError::PresenceTooLong);
133	}
134	if request.status_msg.as_ref().is_some_and(|status| status.len() > MAX_STATUS_LEN) {
135		return Err(SetPresenceError::StatusTooLong);
136	}
137	database
138		.upsert_presence_row(
139			claims.user_id.as_str(),
140			request.status_msg.clone(),
141			request.presence.to_string(),
142		)
143		.await
144		.unrecoverable_error("Writing presence to database")?;
145	let response = set_presence::v3::Response::new();
146	Ok(response.into())
147}
148
149/// Component that implements the matrix presence API
150#[derive(Debug)]
151pub struct PresenceComponent;
152
153impl Component for PresenceComponent {
154	fn router() -> Router<ServerState> {
155		Router::new()
156			.route("/_matrix/client/v3/presence/{userId}/status", get(get_presence_handler))
157			.route("/_matrix/client/v3/presence/{userId}/status", put(set_presence_handler))
158	}
159}