Skip to main content

arminius_server_client/handlers/
account_data.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Implements APIs for Client Config from the Matrix spec (See
6//! <https://spec.matrix.org/latest/client-server-api/#client-config>).
7
8#![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	/// Unrecoverable error.
43	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
44	#[error(transparent)]
45	InternalServerError(#[from] InternalServerError),
46	#[error(transparent)]
47	AccountActionNotAuthorized(#[from] auth::AccountActionNotAuthorized),
48	/// Unset account data was queried.
49	#[error("Account data not found")]
50	#[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
51	AccountDataNotFound,
52}
53
54/// Handler for `GET
55/// /_matrix/client/v3/user/{userId}/rooms/{roomId}/account_data/{type}`
56/// (See <https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3useruseridroomsroomidaccount_datatype>).
57///
58/// This endpoint is used to get room account data by its type.
59async 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
79/// Handler for `GET /_matrix/client/v3/user/{userId}/account_data/{type}`
80/// (See <https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3useruseridaccount_datatype>).
81///
82/// This endpoint is used to get global account data by its type.
83async 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	/// Unrecoverable error.
111	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
112	#[error(transparent)]
113	InternalServerError(#[from] InternalServerError),
114	#[error(transparent)]
115	InputJsonError(#[from] InputJsonError),
116	#[error(transparent)]
117	AccountActionNotAuthorized(#[from] auth::AccountActionNotAuthorized),
118	/// A user tried to modify a server only event (like `m.push_rules` and
119	/// similar).
120	#[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
125/// Returns an error if users are not allowed to set account data events with
126/// the given `event_type`.
127///
128/// Here is the list of forbidden types:
129/// - [ruma::events::GlobalAccountDataEventType::PushRules]
130/// - [ruma::events::RoomAccountDataEventType::FullyRead]
131fn 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
140/// Handler for `PUT
141/// /_matrix/client/v3/user/{userId}/rooms/{roomId}/account_data/{type}`
142/// (See <https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3useruseridroomsroomidaccount_datatype>).
143///
144/// This endpoint is used to set account data.
145///
146/// NOTE: Any existing data of the same type and for the same room will get
147/// overwritten.
148async 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
181/// Handler for `PUT /_matrix/client/v3/user/{userId}/account_data/{type}`
182/// (See <https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3useruseridaccount_datatype>).
183///
184/// Endpoint for setting global account data.
185///
186/// NOTE: Any existing data of the same type will get overwritten.
187async 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/// The top-level router object for this module
220#[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}