arminius_server_client/handlers/
account_data.rs1#![allow(
9 clippy::allow_attributes,
10 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."
11)]
12
13use arminius_lib_component::{
14 Component, ServerState,
15 error::{InputJsonError, InternalServerError, MatrixApiError, ResultExt as _},
16 ruma_requests::ArminiusResponse,
17};
18use arminius_lib_db::DefaultDb;
19use arminius_lib_db_account_data::AccountDataQueries as _;
20use arminius_lib_macros::FromDbError;
21use axum::{Router, extract::State, http::StatusCode, routing::get};
22use ruma::{
23 api::{
24 client::config::{
25 get_global_account_data, get_room_account_data, set_global_account_data,
26 set_room_account_data,
27 },
28 error::ErrorKind,
29 },
30 serde::Raw,
31};
32
33use crate::{auth, client_request::AuthenticatedClientRequest};
34
35#[allow(
36 missing_docs,
37 clippy::missing_docs_in_private_items,
38 reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
39)]
40#[derive(thiserror::Error, Debug, MatrixApiError, FromDbError)]
41enum GetAccountDataError {
42 #[error(transparent)]
45 InternalServerError(#[from] InternalServerError),
46 #[error(transparent)]
47 AccountActionNotAuthorized(#[from] auth::AccountActionNotAuthorized),
48 #[error("Account data not found")]
50 #[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
51 AccountDataNotFound,
52}
53
54async fn get_room_account_data_handler(
60 mut database: State<DefaultDb>,
61 AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
62 get_room_account_data::v3::Request,
63 >,
64) -> Result<ArminiusResponse<get_room_account_data::v3::Response>, GetAccountDataError> {
65 let get_room_account_data::v3::Request { user_id, room_id, event_type, .. } = request;
66
67 auth::check_user_matches(claims.user_id.as_ref(), &user_id)?;
68
69 let event = database
70 .fetch_room_account_data_untyped(&user_id, &room_id, &event_type)
71 .await?
72 .ok_or(GetAccountDataError::AccountDataNotFound)?;
73
74 Ok(ArminiusResponse(get_room_account_data::v3::Response::new(Raw::from_json(
75 serde_json::value::to_raw_value(&event).unrecoverable_error("Converting to raw value")?,
76 ))))
77}
78
79async fn get_global_account_data_handler(
84 mut database: State<DefaultDb>,
85 AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
86 get_global_account_data::v3::Request,
87 >,
88) -> Result<ArminiusResponse<get_global_account_data::v3::Response>, GetAccountDataError> {
89 let get_global_account_data::v3::Request { user_id, event_type, .. } = request;
90
91 auth::check_user_matches(claims.user_id.as_ref(), &user_id)?;
92
93 let event = database
94 .fetch_global_account_data_untyped(&user_id, &event_type)
95 .await?
96 .ok_or(GetAccountDataError::AccountDataNotFound)?;
97
98 Ok(ArminiusResponse(get_global_account_data::v3::Response::new(Raw::from_json(
99 serde_json::value::to_raw_value(&event).unrecoverable_error("Converting to raw value")?,
100 ))))
101}
102
103#[allow(
104 missing_docs,
105 clippy::missing_docs_in_private_items,
106 reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
107)]
108#[derive(thiserror::Error, Debug, MatrixApiError, FromDbError)]
109enum SetAccountDataError {
110 #[error(transparent)]
113 InternalServerError(#[from] InternalServerError),
114 #[error(transparent)]
115 InputJsonError(#[from] InputJsonError),
116 #[error(transparent)]
117 AccountActionNotAuthorized(#[from] auth::AccountActionNotAuthorized),
118 #[error("Only the server is allowed to set this event type")]
121 #[matrix_api_error(status_code = StatusCode::METHOD_NOT_ALLOWED, error_kind = ErrorKind::BadJson)]
122 EventTypeCannotBeSetByClient,
123}
124
125fn check_account_data_event_type_can_be_set_by_client(
132 event_type: &str,
133) -> Result<(), SetAccountDataError> {
134 match event_type {
135 "m.fully_read" | "m.push_rules" => Err(SetAccountDataError::EventTypeCannotBeSetByClient),
136 _ => Ok(()),
137 }
138}
139
140async fn set_room_account_data_handler(
149 mut state: State<ServerState>,
150 AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
151 set_room_account_data::v3::Request,
152 >,
153) -> Result<ArminiusResponse<set_room_account_data::v3::Response>, SetAccountDataError> {
154 let set_room_account_data::v3::Request { user_id, room_id, event_type, data, .. } = request;
155
156 auth::check_user_matches(claims.user_id.as_ref(), &user_id)?;
157 check_account_data_event_type_can_be_set_by_client(&event_type.to_string())?;
158
159 if state
160 .database
161 .upsert_room_account_data(
162 &user_id,
163 &room_id,
164 &event_type,
165 serde_json::to_value(data.into_json()).map_err(InputJsonError::from)?,
166 )
167 .await?
168 .rows_affected()
169 != 0
170 {
171 state
172 .nats_connection
173 .notify_new_sync_data()
174 .await
175 .unrecoverable_error("Failed to notify sync for new account data")?;
176 }
177
178 Ok(set_room_account_data::v3::Response::new().into())
179}
180
181async fn set_global_account_data_handler(
188 mut state: State<ServerState>,
189 AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
190 set_global_account_data::v3::Request,
191 >,
192) -> Result<ArminiusResponse<set_global_account_data::v3::Response>, SetAccountDataError> {
193 let set_global_account_data::v3::Request { user_id, event_type, data, .. } = request;
194
195 auth::check_user_matches(claims.user_id.as_ref(), &user_id)?;
196 check_account_data_event_type_can_be_set_by_client(&event_type.to_string())?;
197
198 if state
199 .database
200 .upsert_global_account_data(
201 &user_id,
202 &event_type,
203 serde_json::to_value(data.clone().into_json()).map_err(InputJsonError::from)?,
204 )
205 .await?
206 .rows_affected()
207 != 0
208 {
209 state
210 .nats_connection
211 .notify_new_sync_data()
212 .await
213 .unrecoverable_error("Failed to notify sync for new account data")?;
214 }
215
216 Ok(set_global_account_data::v3::Response::new().into())
217}
218
219#[derive(Debug)]
221pub struct AccountDataComponent;
222
223impl Component for AccountDataComponent {
224 fn router() -> Router<ServerState> {
225 Router::new()
226 .route(
227 "/_matrix/client/v3/user/{userId}/account_data/{type}",
228 get(get_global_account_data_handler).put(set_global_account_data_handler),
229 )
230 .route(
231 "/_matrix/client/v3/user/{userId}/rooms/{roomId}/account_data/{type}",
232 get(get_room_account_data_handler).put(set_room_account_data_handler),
233 )
234 }
235}