Skip to main content

arminius_lib_sync/syncv4/plan/
apply_request.rs

1// SPDX-FileCopyrightText: 2026 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Contains logic to apply a sync v4 request to a user's rooms.
6
7mod tests;
8
9use std::collections::{BTreeMap, HashMap};
10
11use arminius_lib_component::error::ResultExt as _;
12use arminius_lib_db_sync::syncv4::RoomInfo;
13use arminius_lib_model::syncv4::RoomConfig;
14use itertools::Itertools as _;
15use ruma::{OwnedRoomId, api::client::sync::sync_events};
16
17use crate::syncv4::error::SyncV4Error;
18
19/// Room matched by a request, produced by [`apply_request`].
20#[derive(Debug)]
21pub struct MatchedRoom {
22	/// The (possibly combined) room config to apply to the room.
23	pub(crate) room_config: RoomConfig,
24	/// Map of all the rooms the user is or has been a member of to their information.
25	pub(crate) room_info: RoomInfo,
26}
27
28/// Result produced by [`apply_request`].
29pub struct FilteredRooms {
30	/// The counts of rooms matched by each list.
31	pub(crate) lists_counts: HashMap<String, usize>,
32
33	/// The map of matched rooms (by either lists or subscriptions) to their [`MatchedRoom`].
34	pub(crate) matched_rooms: HashMap<OwnedRoomId, MatchedRoom>,
35}
36
37/// Applies a request's lists and subscription to the list of rooms the user is or has been in.
38pub(super) fn apply_request(
39	user_rooms: &HashMap<OwnedRoomId, RoomInfo>,
40	req_lists: &BTreeMap<String, sync_events::v5::request::List>,
41	req_subscriptions: &BTreeMap<OwnedRoomId, sync_events::v5::request::RoomSubscription>,
42) -> Result<FilteredRooms, SyncV4Error> {
43	let mut matched_rooms = HashMap::new();
44
45	let lists_counts = apply_lists(user_rooms, req_lists, &mut matched_rooms)?;
46	apply_subscriptions(user_rooms, req_subscriptions, &mut matched_rooms);
47
48	Ok(FilteredRooms { lists_counts, matched_rooms })
49}
50
51/// Applies the request's lists.
52///
53/// NOTE: This updates `result` in place for every matched room.
54///
55/// Returns the count of rooms that matched each list.
56fn apply_lists(
57	user_rooms: &HashMap<OwnedRoomId, RoomInfo>,
58	lists: &BTreeMap<String, sync_events::v5::request::List>,
59	result: &mut HashMap<OwnedRoomId, MatchedRoom>,
60) -> Result<HashMap<String, usize>, SyncV4Error> {
61	let mut lists_counts = HashMap::new();
62
63	// Sort the rooms by their most recent event's stream ID. Use this to
64	// determine if they belong to the request's lists ranges.
65	let sorted_rooms: Vec<(&OwnedRoomId, &RoomInfo)> = user_rooms
66		.iter()
67		.sorted_by_key(|(_room_id, room_info)| std::cmp::Reverse(room_info.max_event_stream_id))
68		.collect();
69
70	for (list_name, list) in lists {
71		let room_config: RoomConfig = list.room_details.clone().into();
72
73		// FIXME(#2197): Apply filters
74		let filtered_rooms = user_rooms;
75
76		// Store the count of rooms that were matched by this list's filters
77		lists_counts.insert(list_name.clone(), filtered_rooms.len());
78
79		// If we detect the list actually lists nothing, we should ignore it
80		if list.ranges.is_empty() && list.filters.is_none() {
81			tracing::warn!("This list doesn't list anything: {}", list_name);
82			continue;
83		}
84
85		// If no ranges, every room is matched
86		if list.ranges.is_empty() {
87			for (room_id, room_info) in filtered_rooms {
88				result
89					.entry(room_id.to_owned())
90					.and_modify(|entry| entry.room_config.combine(&room_config))
91					.or_insert_with(|| MatchedRoom {
92						room_config: room_config.clone(),
93						room_info: room_info.clone(),
94					});
95			}
96		}
97		// Otherwise, only consider the rooms matched by each range
98		else {
99			for (range_start, range_end) in list.ranges.clone() {
100				let (range_start, range_end): (usize, usize) = (
101					range_start.try_into().unrecoverable_error("Converting UInt to usize")?,
102					range_end.try_into().unrecoverable_error("Converting UInt to usize")?,
103				);
104
105				for (room_id, room_info) in sorted_rooms
106					.iter()
107					.skip(range_start)
108					.take(range_end.saturating_sub(range_start) + 1)
109				{
110					result
111						.entry((**room_id).clone())
112						.and_modify(|entry| entry.room_config.combine(&room_config))
113						.or_insert_with(|| MatchedRoom {
114							room_config: room_config.clone(),
115							room_info: (**room_info).clone(),
116						});
117				}
118			}
119		}
120	}
121
122	Ok(lists_counts)
123}
124
125/// Applies the request's rooms subscriptions.
126///
127/// NOTE: This updates `result` in place for every matched room.
128fn apply_subscriptions(
129	user_rooms: &HashMap<OwnedRoomId, RoomInfo>,
130	subscriptions: &BTreeMap<OwnedRoomId, sync_events::v5::request::RoomSubscription>,
131	result: &mut HashMap<OwnedRoomId, MatchedRoom>,
132) {
133	for (room_id, room_subscription) in subscriptions {
134		let Some(room_info) = user_rooms.get(room_id) else {
135			// Rooms that users do not have any membership in should be
136			// ignored. See <https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/4186-simplified-sliding-sync.md#subscriptions>
137			tracing::warn!(
138				%room_id,
139				"User tried to subscribe to room {room_id} without being a member of it. Ignoring it.",
140			);
141			continue;
142		};
143
144		let room_config: RoomConfig = room_subscription.clone().into();
145
146		result
147			.entry(room_id.clone())
148			.and_modify(|entry| {
149				entry.room_config.combine(&room_config);
150			})
151			.or_insert_with(|| MatchedRoom { room_config, room_info: room_info.clone() });
152	}
153}