Skip to main content

tuwunel_api/client/sync/v5/
range.rs

1use std::{collections::BTreeMap, mem::take};
2
3use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join4};
4use ruma::{
5	OwnedRoomId, OwnedUserId, RoomId,
6	api::client::sync::sync_events::v5::response,
7	events::{
8		AnyRawAccountDataEvent, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent,
9		GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent,
10		receipt::SyncReceiptEvent,
11	},
12	serde::Raw,
13};
14use tokio::sync::OnceCell;
15use tuwunel_core::{
16	Error, Result, at, err, error, extract_variant, implement,
17	result::NotFound,
18	utils::{BoolExt, IterStream, TryReadyExt, stream::BroadbandExt},
19};
20use tuwunel_service::{
21	rooms::read_receipt::{PrivateReadEvents, pack_receipts_fallible},
22	sync::{Connection, Room, RoomConfig},
23};
24
25use super::{
26	SyncInfo, Window, WindowRoom,
27	rooms::{
28		Failure as RoomFailure,
29		Failure::{Payload as PayloadFailure, Timeline as TimelineFailure},
30		RoomDetails, handle_room, membership_allows_required_state, merged_room_details,
31		room_config,
32	},
33};
34use crate::client::is_empty_account_data_event;
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37enum Domain {
38	Timeline,
39	Payload,
40	PublicReceipt,
41	PrivateRead,
42	RoomAccountData,
43	ReceiptSerialization,
44}
45
46#[derive(Debug)]
47struct Failure {
48	domain: Domain,
49	error: Error,
50}
51
52impl Failure {
53	fn new(domain: Domain, error: Error) -> Self { Self { domain, error } }
54}
55
56impl From<RoomFailure> for Failure {
57	fn from(failure: RoomFailure) -> Self {
58		match failure {
59			| TimelineFailure(error) => Self::new(Domain::Timeline, error),
60			| PayloadFailure(error) => Self::new(Domain::Payload, error),
61		}
62	}
63}
64
65#[derive(Debug)]
66struct CompleteRange {
67	payload: Option<response::Room>,
68	config: Option<RoomConfig>,
69	receipts: Option<Raw<SyncReceiptEvent>>,
70	account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
71}
72
73#[derive(Default)]
74pub(super) struct Results {
75	ranges: BTreeMap<OwnedRoomId, CompleteRange>,
76}
77
78#[implement(Results)]
79pub(super) fn room_updates(&mut self) -> impl Iterator<Item = (&RoomId, Option<RoomConfig>)> {
80	self.ranges
81		.iter_mut()
82		.map(|(room_id, range)| (room_id.as_ref(), range.config.take()))
83}
84
85#[implement(Results)]
86pub(super) fn payload(&self, room_id: &RoomId) -> Option<&response::Room> {
87	self.ranges
88		.get(room_id)
89		.and_then(|range| range.payload.as_ref())
90}
91
92#[implement(Results)]
93pub(super) fn into_payloads(self) -> BTreeMap<OwnedRoomId, response::Room> {
94	self.ranges
95		.into_iter()
96		.filter_map(|(room_id, range)| range.payload.map(|payload| (room_id, payload)))
97		.collect()
98}
99
100#[implement(Results)]
101pub(super) fn take_receipts(&mut self, room_id: &RoomId) -> Option<Raw<SyncReceiptEvent>> {
102	self.ranges
103		.get_mut(room_id)
104		.and_then(|range| range.receipts.take())
105}
106
107#[implement(Results)]
108pub(super) fn take_account_data(
109	&mut self,
110	room_id: &RoomId,
111) -> Option<Vec<Raw<AnyRoomAccountDataEvent>>> {
112	self.ranges
113		.get_mut(room_id)
114		.map(|range| take(&mut range.account_data))
115		.filter(|events| !events.is_empty())
116}
117
118#[tracing::instrument(
119	name = "ranges",
120	level = "debug",
121	skip_all,
122	fields(
123		next_batch = conn.next_batch,
124		window = window.len(),
125	),
126)]
127pub(super) async fn collect(
128	sync_info: SyncInfo<'_>,
129	conn: &Connection,
130	window: &Window,
131) -> Results {
132	let ignored = OnceCell::new();
133	let empty_room = Room::default();
134	let ranges = window
135		.iter()
136		.stream()
137		.broad_filter_map(async |(room_id, window_room)| {
138			let room = conn.rooms.get(room_id).unwrap_or(&empty_room);
139
140			let room_details = merged_room_details(conn, &window_room.lists, room_id);
141
142			match collect_room(sync_info, conn, window_room, room, room_details, &ignored).await {
143				| Ok(range) => Some((room_id.clone(), range)),
144				| Err(Failure { domain, error }) => {
145					error!(
146						%room_id,
147						?domain,
148						roomsince = room.roomsince,
149						next_batch = conn.next_batch,
150						%error,
151						"sliding sync range failed"
152					);
153					None
154				},
155			}
156		})
157		.collect()
158		.await;
159
160	Results { ranges }
161}
162
163async fn collect_room(
164	sync_info: SyncInfo<'_>,
165	conn: &Connection,
166	window_room: &WindowRoom,
167	room: &Room,
168	room_details: RoomDetails,
169	ignored: &OnceCell<Option<IgnoredUserListEvent>>,
170) -> Result<CompleteRange, Failure> {
171	let room_id = &window_room.room_id;
172	let (config_hash, required_state) = room_config(&room_details);
173	let config_changed = room.config_hash != config_hash;
174	let payload_is_fresh = window_room.payload_is_fresh(room.roomsince) || config_changed;
175
176	let payload = payload_is_fresh
177		.then_async(|| {
178			handle_room(sync_info, conn, window_room, room, config_changed, room_details)
179		})
180		.map(Option::transpose)
181		.map_err(Failure::from);
182
183	let public_receipts = public_receipts(sync_info, conn, room_id, room.roomsince, ignored)
184		.map_err(|error| Failure::new(Domain::PublicReceipt, error));
185
186	let private_receipts = private_receipts(sync_info, conn, room_id, room.roomsince)
187		.map_err(|error| Failure::new(Domain::PrivateRead, error));
188
189	let account_data = room_account_data(sync_info, conn, room_id, room.roomsince)
190		.map_err(|error| Failure::new(Domain::RoomAccountData, error));
191
192	let (payload, public_receipts, private_receipts, account_data) =
193		try_join4(payload, public_receipts, private_receipts, account_data).await?;
194
195	let required_state = membership_allows_required_state(window_room.membership.as_ref())
196		.then_some(required_state)
197		.unwrap_or_default();
198
199	assemble(
200		payload,
201		public_receipts,
202		private_receipts,
203		account_data,
204		(config_hash, required_state),
205	)
206}
207
208async fn public_receipts(
209	SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
210	conn: &Connection,
211	room_id: &RoomId,
212	roomsince: u64,
213	ignored: &OnceCell<Option<IgnoredUserListEvent>>,
214) -> Result<impl Iterator<Item = Raw<AnySyncEphemeralRoomEvent>>> {
215	let mut receipts: Vec<(OwnedUserId, Raw<AnySyncEphemeralRoomEvent>)> = services
216		.read_receipt
217		.readreceipts_since_fallible(room_id, roomsince, Some(conn.next_batch))
218		.map_ok(|(user_id, _ts, event)| (user_id.to_owned(), event))
219		.try_collect()
220		.await?;
221
222	if !receipts.is_empty() {
223		let ignored = ignored
224			.get_or_try_init(async || {
225				services
226					.account_data
227					.get_global(sender_user, GlobalAccountDataEventType::IgnoredUserList)
228					.await
229					.optional()
230			})
231			.await?;
232
233		if let Some(ignored) = ignored {
234			receipts.retain(|(user_id, _)| {
235				!ignored
236					.content
237					.ignored_users
238					.contains_key(user_id)
239			});
240		}
241	}
242
243	Ok(receipts.into_iter().map(at!(1)))
244}
245
246async fn private_receipts(
247	SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
248	conn: &Connection,
249	room_id: &RoomId,
250	roomsince: u64,
251) -> Result<PrivateReadEvents> {
252	let update = services
253		.read_receipt
254		.last_privateread_update_fallible(sender_user, room_id)
255		.await?;
256
257	match update {
258		| _ if update <= roomsince => Ok(PrivateReadEvents::new()),
259		| _ if update > conn.next_batch =>
260			Err(err!(Database("Private read advanced beyond the bounded sync range."))),
261		| _ =>
262			services
263				.read_receipt
264				.private_read_get_fallible(room_id, sender_user, update)
265				.await,
266	}
267}
268
269async fn room_account_data(
270	SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
271	conn: &Connection,
272	room_id: &RoomId,
273	roomsince: u64,
274) -> Result<Vec<Raw<AnyRoomAccountDataEvent>>> {
275	services
276		.account_data
277		.changes_since_fallible(Some(room_id), sender_user, roomsince, Some(conn.next_batch))
278		.ready_try_filter_map(|event| Ok(extract_variant!(event, AnyRawAccountDataEvent::Room)))
279		.ready_try_filter(move |event| roomsince != 0 || !is_empty_account_data_event(event))
280		.try_collect()
281		.await
282}
283
284fn assemble<PublicReceipts>(
285	payload: Option<response::Room>,
286	public_receipts: PublicReceipts,
287	private_receipts: PrivateReadEvents,
288	account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
289	config: RoomConfig,
290) -> Result<CompleteRange, Failure>
291where
292	PublicReceipts: Iterator<Item = Raw<AnySyncEphemeralRoomEvent>>,
293{
294	let mut receipts = public_receipts.chain(private_receipts).peekable();
295	let receipts = receipts
296		.peek()
297		.is_some()
298		.then(|| pack_receipts_fallible(receipts))
299		.transpose()
300		.map_err(|error| Failure::new(Domain::ReceiptSerialization, error))?;
301
302	let config = payload.as_ref().map(|_| config);
303
304	Ok(CompleteRange { payload, config, receipts, account_data })
305}
306
307#[cfg(test)]
308mod tests {
309	use std::iter::once;
310
311	use ruma::{api::client::sync::sync_events::v5::response::Room as ResponseRoom, room_id};
312	use serde_json::{json, value::to_raw_value};
313
314	use super::*;
315
316	#[test]
317	fn malformed_receipt_withholds_the_complete_range() {
318		let room_id = room_id!("!receipt:example.com");
319		let malformed = Raw::from_json(
320			to_raw_value(&json!({"content": 5})).expect("test JSON should serialize"),
321		);
322
323		let range = assemble(
324			Some(ResponseRoom::default()),
325			vec![malformed].into_iter(),
326			PrivateReadEvents::new(),
327			Vec::new(),
328			(7, once(11).collect()),
329		);
330
331		let error = range.expect_err("malformed receipt must fail the complete range");
332
333		assert_eq!(error.domain, Domain::ReceiptSerialization);
334		assert!(
335			!publish(room_id, Err(error))
336				.ranges
337				.contains_key(room_id)
338		);
339	}
340
341	#[test]
342	fn extension_only_range_commits_without_a_room_payload() {
343		let room_id = room_id!("!extension-only:example.com");
344		let range = assemble(
345			None,
346			Vec::new().into_iter(),
347			PrivateReadEvents::new(),
348			Vec::new(),
349			(7, once(11).collect()),
350		);
351
352		let mut range = publish(room_id, range); // room_updates consumes the configuration.
353
354		assert_eq!(range.room_updates().collect::<Vec<_>>(), [(room_id, None)]);
355		assert!(range.into_payloads().is_empty());
356	}
357
358	#[test]
359	fn extension_outputs_are_taken_once_without_removing_the_complete_range() {
360		let room_id = room_id!("!extension-output:example.com");
361		let receipt = Raw::from_json(
362			to_raw_value(&json!({"content": {}})).expect("test receipt should serialize"),
363		);
364
365		let account_data = Raw::from_json(
366			to_raw_value(&json!({"type": "m.tag", "content": {"tags": {}}}))
367				.expect("test account data should serialize"),
368		);
369
370		let range = CompleteRange {
371			payload: None,
372			config: None,
373			receipts: Some(receipt),
374			account_data: vec![account_data],
375		};
376
377		let ranges = [(room_id.to_owned(), range)].into();
378		let mut results = Results { ranges };
379
380		assert!(results.take_receipts(room_id).is_some());
381		assert!(results.take_receipts(room_id).is_none());
382
383		let count = results
384			.take_account_data(room_id)
385			.map(|events| events.len());
386
387		assert_eq!(Some(1), count);
388		assert!(results.take_account_data(room_id).is_none());
389		assert!(results.ranges.contains_key(room_id));
390	}
391
392	fn publish(room_id: &RoomId, range: Result<CompleteRange, Failure>) -> Results {
393		let ranges = range
394			.ok()
395			.map(|range| (room_id.to_owned(), range))
396			.into_iter()
397			.collect();
398
399		Results { ranges }
400	}
401}