Skip to main content

arminius_server_client/handlers/
filter.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Implements handlers for filters (See <https://spec.matrix.org/v1.14/client-server-api/#filtering>).
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 arminius_lib_authentication::SessionInfo;
15use arminius_lib_component::{
16	Component, ServerState,
17	error::{
18		FeatureNotImplemented, InputJsonError, InternalServerError, MatrixApiError, ResultExt as _,
19	},
20	ruma_requests::ArminiusResponse,
21};
22use arminius_lib_db::DefaultDb;
23use arminius_lib_db_filters::FilterQueries as _;
24use axum::{
25	Router,
26	extract::State,
27	routing::{get, post},
28};
29use reqwest::StatusCode;
30use ruma::api::{
31	client::filter::{create_filter, get_filter},
32	error::ErrorKind,
33};
34
35use crate::client_request::AuthenticatedClientRequest;
36
37/// Errors that can be returned by the filter handlers.
38#[allow(
39	missing_docs,
40	clippy::missing_docs_in_private_items,
41	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
42)]
43#[derive(Debug, thiserror::Error, MatrixApiError)]
44enum FilterError {
45	#[error(transparent)]
46	InternalServerError(#[from] InternalServerError),
47
48	#[error(transparent)]
49	FeatureNotImplemented(#[from] FeatureNotImplemented),
50
51	#[error(transparent)]
52	InputJsonError(#[from] InputJsonError),
53
54	/// The filter cannot be added to or received from a user other than the one
55	/// authenticated.
56	#[error("The user ID in the param does not match the user ID of the authenticated user")]
57	#[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
58	UserIdMismatch,
59
60	/// Filter ID on our homeserver is the nid of the row which is `i64` number.
61	/// if it fails to parse, it means that the provided filter ID is not a
62	/// valid one.
63	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidParam)]
64	#[error("The filter ID provided is not a valid filter ID")]
65	InvalidFilterId,
66
67	/// Filter that was requested cannot be found.
68	#[error("Filter not found")]
69	#[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
70	UnknownFilter,
71}
72
73/// Handler for `POST /_matrix/client/v3/user/{userId}/filter` (See
74/// <https://spec.matrix.org/v1.18/client-server-api/#post_matrixclientv3useruseridfilter>).
75///
76/// Used to create new filter definitions.
77async fn create_filter_handler(
78	mut database: State<DefaultDb>,
79	AuthenticatedClientRequest(SessionInfo { user_id, device_id }, request): AuthenticatedClientRequest<create_filter::v3::Request>,
80) -> Result<ArminiusResponse<create_filter::v3::Response>, FilterError> {
81	if request.user_id != user_id {
82		return Err(FilterError::UserIdMismatch);
83	}
84
85	let filter_id = database
86		.insert_filter(&user_id, &device_id, request.filter.clone())
87		.await
88		.unrecoverable_error("Fetching filter from database failed")?
89		.to_string();
90
91	Ok(create_filter::v3::Response::new(filter_id).into())
92}
93
94/// Handler for `GET /_matrix/client/v3/user/{userId}/filter/{filterId}` (See
95/// <https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3useruseridfilterfilterid>).
96///
97/// Used to get existing filter definitions.
98async fn get_filter_handler(
99	mut database: State<DefaultDb>,
100	AuthenticatedClientRequest(SessionInfo { user_id, .. }, request): AuthenticatedClientRequest<
101		get_filter::v3::Request,
102	>,
103) -> Result<ArminiusResponse<get_filter::v3::Response>, FilterError> {
104	if request.user_id != user_id {
105		return Err(FilterError::UserIdMismatch);
106	}
107
108	let filter_id = request.filter_id.parse().map_err(|_| FilterError::InvalidFilterId)?;
109
110	let filter = database
111		.fetch_filter_with_id(filter_id, &user_id)
112		.await
113		.unrecoverable_error("Fetching filter from the database failed")?;
114
115	let Some(filter) = filter else { return Err(FilterError::UnknownFilter) };
116
117	Ok(ArminiusResponse::from(get_filter::v3::Response::new(filter)))
118}
119
120/// Component for the filter configuration
121#[derive(Debug)]
122pub struct FilterComponent;
123
124impl Component for FilterComponent {
125	fn router() -> Router<ServerState> {
126		Router::new()
127			.route("/_matrix/client/v3/user/{userId}/filter", post(create_filter_handler))
128			.route("/_matrix/client/v3/user/{userId}/filter/{filterId}", get(get_filter_handler))
129	}
130}