arminius_server_client/handlers/
presence.rs1#![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
42const MAX_STATUS_LEN: usize = 255;
45const MAX_PRESENCE_LEN: usize = 100;
47
48async 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 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 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 #[error(transparent)]
100 InternalServerError(#[from] InternalServerError),
101 #[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("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
113async 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#[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}