Skip to main content

arminius_lib_model/
filter.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Data models related to Filters
6
7use std::ops::Not as _;
8
9use arminius_lib_db::{DatabaseError, ResultExt as _};
10use ruma::{
11	OwnedDeviceId, RoomId, UInt, UserId,
12	api::client::filter::{
13		EventFormat, Filter, FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter,
14		UrlFilter,
15	},
16};
17use sqlx::{Type, prelude::FromRow};
18
19use crate::types::DbUlid;
20
21/// Filters applied to the room events, based on which events these are
22/// used. See [`DbRoomFilter`] for more.
23#[derive(Debug, Type)]
24#[sqlx(type_name = "room_event_filter")]
25pub struct DbRoomEventFilter {
26	/// The maximum number of events returned.
27	/// It should always be more than 0 and and have a reasonable maximum.
28	///
29	/// NOTE: this field should never return `None`, but since the type
30	/// checking by sqlx does not allow for overwrite or constraint on the
31	/// types, we will handle it at the application level.
32	limit: Option<i32>,
33	/// The rooms to exclude.
34	not_rooms: Option<Vec<String>>,
35	/// The senders to exclude.
36	not_senders: Option<Vec<String>>,
37	/// The event types to exclude.
38	not_types: Option<Vec<String>>,
39	/// The rooms to include.
40	rooms: Option<Vec<String>>,
41	/// The senders to include.
42	senders: Option<Vec<String>>,
43	/// The types to include.
44	types: Option<Vec<String>>,
45	/// If it `Some`, it means that something events will be excluded.
46	/// If it is `true` the events without URL will be excluded.
47	/// If it is `false` the events with URL will be excluded.
48	contains_url: Option<bool>,
49	/// If `true`, sends all membership events for all events, even if they
50	/// have already been sent to the client. Does not apply unless
51	/// `lazy_load_members` is `true`.
52	include_redundant_members: Option<bool>,
53	/// Lazily Load the membership events.
54	lazy_load_members: Option<bool>,
55	/// Enables Per thread notification counts. Only applies to the `/sync`
56	/// endpoint.
57	unread_thread_notifications: Option<bool>,
58}
59
60/// Filters that apply to the room events.
61#[derive(Debug, Type)]
62#[sqlx(type_name = "room_filter")]
63pub struct DbRoomFilter {
64	/// The per user account data to include for rooms.
65	pub account_data: Option<DbRoomEventFilter>,
66	/// Ephemeral events to include for rooms. appear in the ephemeral
67	/// property in the `/sync` end-point.
68	pub ephemeral: Option<DbRoomEventFilter>,
69	/// The state events to include for the rooms.
70	pub state: Option<DbRoomEventFilter>,
71	/// The message and state update events to include for rooms.
72	pub timeline: Option<DbRoomEventFilter>,
73	/// Include rooms that the user has left in the sync.
74	pub include_leave: Option<bool>,
75	/// Events from these rooms should be excluded.
76	pub not_rooms: Option<Vec<String>>,
77	/// Events from these rooms should be included.
78	pub rooms: Option<Vec<String>>,
79}
80
81/// General Event filters.
82#[derive(Debug, Type)]
83#[sqlx(type_name = "event_filter")]
84pub struct DbEventFilter {
85	/// The Limit of events returned.
86	/// It should always be more than 0 and and have a reasonable maximum.
87	///
88	/// NOTE: this field should never return `None`, but since the type
89	/// checking by sqlx does not allow for overwrite or constraint on the
90	/// types, we will handle it at the application level.
91	pub limit: Option<i32>,
92	/// Events from these senders should be excluded.
93	pub not_senders: Option<Vec<String>>,
94	/// Events with these types should be excluded.
95	pub not_types: Option<Vec<String>>,
96	/// Events from these senders should be included.
97	pub senders: Option<Vec<String>>,
98	/// Events with these types should be included.
99	pub types: Option<Vec<String>>,
100}
101
102/// Format of the event that is requested by the client in the filters.
103/// This is defaulted to client.
104#[derive(Debug, Type)]
105#[sqlx(type_name = "event_format_enum", rename_all = "lowercase")]
106pub enum DbEventFormat {
107	/// The events should be send using formats appropriate for the clients.
108	Client,
109	/// The events should be send in raw format, as received by the
110	/// homeserver.
111	Federation,
112}
113
114impl TryFrom<DbRoomEventFilter> for Filter {
115	type Error = DatabaseError;
116
117	fn try_from(filter: DbRoomEventFilter) -> Result<Self, Self::Error> {
118		let mut ruma_filter = Self::empty();
119		ruma_filter.not_types = filter.not_types.unwrap_or_default();
120		ruma_filter.limit = filter
121			.limit
122			.map(|limit| {
123				UInt::try_from(limit).invalid_data(&format!(
124					"the limit number of '{limit}' is greater than the maximum number that can fit into a non negative f64"
125				))
126			})
127			.transpose()?;
128		if let Some(senders) = filter.senders {
129			ruma_filter.senders = Some(
130				senders
131					.into_iter()
132					.map(|raw_user_id| {
133						UserId::parse(&raw_user_id).invalid_data(&format!(
134							"the user_id retrieved from the database for '{raw_user_id}' is not a valid user ID"
135						))
136					})
137					.collect::<Result<Vec<_>, _>>()?,
138			);
139		}
140		ruma_filter.not_senders = filter
141			.not_senders
142			.unwrap_or_default()
143			.into_iter()
144			.map(|raw_user_id| {
145				UserId::parse(&raw_user_id).invalid_data(&format!(
146					"the user_id retrieved from the database for '{raw_user_id}' is not a valid user ID"
147				))
148			})
149			.collect::<Result<Vec<_>, _>>()?;
150
151		ruma_filter.types = filter.types;
152		Ok(ruma_filter)
153	}
154}
155
156impl From<DbEventFormat> for EventFormat {
157	fn from(event_format: DbEventFormat) -> Self {
158		match event_format {
159			DbEventFormat::Client => EventFormat::Client,
160			DbEventFormat::Federation => EventFormat::Federation,
161		}
162	}
163}
164
165impl TryFrom<DbRoomEventFilter> for RoomEventFilter {
166	type Error = DatabaseError;
167
168	fn try_from(room_event_filter: DbRoomEventFilter) -> Result<Self, Self::Error> {
169		let mut ruma_room_event_filter = Self::empty();
170		ruma_room_event_filter.types = room_event_filter.types;
171		ruma_room_event_filter.not_types = room_event_filter.not_types.unwrap_or_default();
172		ruma_room_event_filter.not_rooms = room_event_filter
173			.not_rooms
174			.unwrap_or_default()
175			.into_iter()
176			.map(|raw_room_id| {
177				RoomId::parse(&raw_room_id).invalid_data(&format!(
178					"the room id from database '{raw_room_id}' is not a valid room id"
179				))
180			})
181			.collect::<Result<Vec<_>, _>>()?;
182		ruma_room_event_filter.rooms = room_event_filter
183			.rooms
184			.map(|rooms| {
185				rooms
186					.into_iter()
187					.map(|raw_room_id| {
188						RoomId::parse(&raw_room_id).invalid_data(&format!(
189							"the room id from database '{raw_room_id}' is not a valid room id"
190						))
191					})
192					.collect::<Result<Vec<_>, _>>()
193			})
194			.transpose()?;
195
196		ruma_room_event_filter.not_senders = room_event_filter
197			.not_senders
198			.unwrap_or_default()
199			.into_iter()
200			.map(|raw_user_id| {
201				UserId::parse(&raw_user_id).invalid_data(&format!(
202					"the user id from database '{raw_user_id}' is not a valid user id"
203				))
204			})
205			.collect::<Result<Vec<_>, _>>()?;
206
207		ruma_room_event_filter.senders = room_event_filter
208			.senders
209			.map(|senders| {
210				senders
211					.into_iter()
212					.map(|raw_user_id| {
213						UserId::parse(&raw_user_id).invalid_data(&format!(
214							"the user id from database '{raw_user_id}' is not a valid user id"
215						))
216					})
217					.collect::<Result<Vec<_>, _>>()
218			})
219			.transpose()?;
220		ruma_room_event_filter.limit =
221			room_event_filter.limit
222			.map(|limit| {
223				UInt::try_from(limit).invalid_data(&format!(
224					"the limit number of '{limit}' is greater than the maximum number that can fit into a non negative f64"
225				))
226			}).transpose()?;
227		ruma_room_event_filter.url_filter = room_event_filter.contains_url.map(|contains_url| {
228			if contains_url { UrlFilter::EventsWithUrl } else { UrlFilter::EventsWithoutUrl }
229		});
230		ruma_room_event_filter.lazy_load_options =
231			if room_event_filter.lazy_load_members.is_some_and(|lazy_load| lazy_load) {
232				LazyLoadOptions::Enabled {
233					include_redundant_members: room_event_filter
234						.include_redundant_members
235						.unwrap_or_default(),
236				}
237			} else {
238				LazyLoadOptions::Disabled
239			};
240
241		if let Some(unread_thread_notifications) = room_event_filter.unread_thread_notifications {
242			ruma_room_event_filter.unread_thread_notifications = unread_thread_notifications;
243		}
244		Ok(ruma_room_event_filter)
245	}
246}
247
248impl TryFrom<DbRoomFilter> for RoomFilter {
249	type Error = DatabaseError;
250
251	fn try_from(room_filter: DbRoomFilter) -> Result<Self, Self::Error> {
252		let mut ruma_room_filter = Self::empty();
253		ruma_room_filter.include_leave = room_filter.include_leave.unwrap_or_default();
254
255		if let Some(account_data) = room_filter.account_data {
256			ruma_room_filter.account_data = account_data.try_into()?;
257		}
258		if let Some(ephemeral) = room_filter.ephemeral {
259			ruma_room_filter.ephemeral = ephemeral.try_into()?;
260		}
261		if let Some(state) = room_filter.state {
262			ruma_room_filter.state = state.try_into()?;
263		}
264		if let Some(timeline) = room_filter.timeline {
265			ruma_room_filter.timeline = timeline.try_into()?;
266		}
267		ruma_room_filter.rooms = room_filter
268			.rooms
269			.map(|rooms| {
270				rooms
271					.into_iter()
272					.map(|raw_room_id| {
273						RoomId::parse(&raw_room_id).invalid_data(&format!(
274							"the room id from database '{raw_room_id}' is not a valid room id"
275						))
276					})
277					.collect::<Result<Vec<_>, _>>()
278			})
279			.transpose()?;
280		ruma_room_filter.not_rooms = room_filter
281			.not_rooms
282			.unwrap_or_default()
283			.into_iter()
284			.map(|raw_room_id| {
285				RoomId::parse(&raw_room_id).invalid_data(&format!(
286					"the room id from database '{raw_room_id}' is not a valid room id"
287				))
288			})
289			.collect::<Result<Vec<_>, _>>()?;
290		Ok(ruma_room_filter)
291	}
292}
293
294impl From<EventFormat> for DbEventFormat {
295	fn from(format: EventFormat) -> Self {
296		match format {
297			EventFormat::Federation => DbEventFormat::Federation,
298			_ => DbEventFormat::Client, // Catch-all for forward compatibility
299		}
300	}
301}
302
303impl From<Filter> for DbEventFilter {
304	fn from(filter: Filter) -> Self {
305		Self {
306			limit: filter.limit.map(|limit| limit.try_into().unwrap_or(i32::MAX)),
307			not_senders: filter
308				.not_senders
309				.is_empty()
310				.not()
311				.then(|| filter.not_senders.into_iter().map(|sender| sender.to_string()).collect()),
312			not_types: filter.not_types.is_empty().not().then_some(filter.not_types),
313			senders: filter
314				.senders
315				.map(|sender| sender.into_iter().map(|id| id.to_string()).collect()),
316			types: filter.types,
317		}
318	}
319}
320
321impl From<RoomEventFilter> for DbRoomEventFilter {
322	fn from(filter: RoomEventFilter) -> Self {
323		let (lazy_load_members, include_redundant_members) = match filter.lazy_load_options {
324			LazyLoadOptions::Enabled { include_redundant_members } => {
325				(Some(true), Some(include_redundant_members))
326			}
327			LazyLoadOptions::Disabled => (Some(false), Some(false)),
328		};
329
330		Self {
331			limit: filter.limit.map(|limit| limit.try_into().unwrap_or(i32::MAX)),
332			not_rooms: filter
333				.not_rooms
334				.is_empty()
335				.not()
336				.then(|| filter.not_rooms.into_iter().map(|id| id.to_string()).collect()),
337			not_senders: filter
338				.not_senders
339				.is_empty()
340				.not()
341				.then(|| filter.not_senders.into_iter().map(|id| id.to_string()).collect()),
342			not_types: filter.not_types.is_empty().not().then_some(filter.not_types),
343			rooms: filter.rooms.map(|room| room.into_iter().map(|id| id.to_string()).collect()),
344			senders: filter
345				.senders
346				.map(|sender| sender.into_iter().map(|id| id.to_string()).collect()),
347			types: filter.types,
348			contains_url: match filter.url_filter {
349				Some(UrlFilter::EventsWithUrl) => Some(true),
350				Some(UrlFilter::EventsWithoutUrl) => Some(false),
351				None => None,
352			},
353			include_redundant_members,
354			lazy_load_members,
355			unread_thread_notifications: Some(filter.unread_thread_notifications),
356		}
357	}
358}
359
360impl From<RoomFilter> for DbRoomFilter {
361	fn from(filter: RoomFilter) -> Self {
362		Self {
363			account_data: filter.account_data.is_empty().not().then(|| filter.account_data.into()),
364			ephemeral: filter.ephemeral.is_empty().not().then(|| filter.ephemeral.into()),
365			state: filter.state.is_empty().not().then(|| filter.state.into()),
366			timeline: filter.timeline.is_empty().not().then(|| filter.timeline.into()),
367			include_leave: Some(filter.include_leave),
368			not_rooms: filter
369				.not_rooms
370				.is_empty()
371				.not()
372				.then(|| filter.not_rooms.into_iter().map(|id| id.to_string()).collect()),
373			rooms: filter.rooms.map(|room| room.into_iter().map(|id| id.to_string()).collect()),
374		}
375	}
376}
377
378/// A struct that does the job of receiving the filter.
379#[derive(Debug, FromRow)]
380pub struct FilterRow {
381	/// The ID assigned by the database for this record. Later used
382	/// for recovering the data.
383	pub id: DbUlid,
384	/// UserId of the creator of the filter. Only this user should have
385	/// access to this field.
386	pub user_id: String,
387	/// DeviceId of the device that requested this filter. The lifetime of
388	/// the filter depends on the lifetime of the device in the database.
389	pub device_id: OwnedDeviceId,
390	/// Filters that apply to the room events.
391	pub room: Option<DbRoomFilter>,
392	/// Filters that apply to the account data.
393	pub account_data: Option<DbEventFilter>,
394	/// Filters that apply to the presence data.
395	pub presence: Option<DbEventFilter>,
396	/// Format of the events returned.
397	pub event_format: DbEventFormat,
398	/// fields of the the events to return.
399	pub event_fields: Option<Vec<String>>,
400}
401
402impl TryFrom<DbEventFilter> for Filter {
403	type Error = DatabaseError;
404
405	fn try_from(filter: DbEventFilter) -> Result<Self, Self::Error> {
406		let mut ruma_filter = Self::empty();
407		ruma_filter.not_types = filter.not_types.unwrap_or_default();
408		ruma_filter.limit = filter
409			.limit
410			.map(|limit| {
411				UInt::try_from(limit).invalid_data(&format!(
412					"the limit number of '{limit}' is greater than the maximum number that can fit into a non negative f64"
413				))
414			})
415			.transpose()?;
416		if let Some(senders) = filter.senders {
417			ruma_filter.senders = Some(
418				senders
419					.into_iter()
420					.map(|raw_user_id| {
421						UserId::parse(&raw_user_id).invalid_data(&format!(
422							"the user_id retrieved from the database for '{raw_user_id}' is not a valid user ID"
423						))
424					})
425					.collect::<Result<Vec<_>, _>>()?,
426			);
427		}
428		ruma_filter.not_senders = filter
429			.not_senders
430			.unwrap_or_default()
431			.into_iter()
432			.map(|raw_user_id| {
433				UserId::parse(&raw_user_id).invalid_data(&format!(
434					"the user_id retrieved from the database for '{raw_user_id}' is not a valid user ID"
435				))
436			})
437			.collect::<Result<Vec<_>, _>>()?;
438
439		ruma_filter.types = filter.types;
440		Ok(ruma_filter)
441	}
442}
443
444impl TryFrom<FilterRow> for FilterDefinition {
445	type Error = DatabaseError;
446
447	fn try_from(row: FilterRow) -> Result<Self, Self::Error> {
448		let mut filter_definition = FilterDefinition::empty();
449
450		filter_definition.event_fields = row.event_fields;
451		filter_definition.event_format = row.event_format.into();
452		if let Some(presence) = row.presence {
453			filter_definition.presence = presence.try_into()?;
454		}
455		if let Some(account_data) = row.account_data {
456			filter_definition.account_data = account_data.try_into()?;
457		}
458		if let Some(room) = row.room {
459			filter_definition.room = room.try_into()?;
460		}
461
462		Ok(filter_definition)
463	}
464}