Skip to main content

tuwunel_api_client/sync/v5/
range.rs

1use std::{collections::BTreeMap, iter::empty, 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, DeliveredConfig, Room, RoomUpdate},
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::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<DeliveredConfig>,
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 = RoomUpdate<'_>> {
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 delivery = 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	if window_room.forgotten {
184		let (payload, state_read) = delivery.map_ok(Option::unzip).await?;
185		let (shortstatehash, unread_state) = state_read.unwrap_or_default();
186		let shortstatehash = shortstatehash.unwrap_or(room.shortstatehash);
187
188		return assemble(
189			payload,
190			empty(),
191			PrivateReadEvents::new(),
192			Vec::new(),
193			(config_hash, Default::default(), shortstatehash, unread_state),
194		);
195	}
196
197	let public_receipts = public_receipts(sync_info, conn, room_id, room.roomsince, ignored)
198		.map_err(|error| Failure::new(Domain::PublicReceipt, error));
199
200	let private_receipts = private_receipts(sync_info, conn, room_id, room.roomsince)
201		.map_err(|error| Failure::new(Domain::PrivateRead, error));
202
203	let account_data = room_account_data(sync_info, conn, room_id, room.roomsince)
204		.map_err(|error| Failure::new(Domain::RoomAccountData, error));
205
206	let (delivery, public_receipts, private_receipts, account_data) =
207		try_join4(delivery, public_receipts, private_receipts, account_data).await?;
208
209	let required_state = membership_allows_required_state(window_room.membership.as_ref())
210		.then_some(required_state)
211		.unwrap_or_default();
212
213	// Without freshly read state, the range keeps the snapshot recorded at its last delivery.
214	let (payload, state_read) = delivery.unzip();
215	let (shortstatehash, unread_state) = state_read.unwrap_or_default();
216	let shortstatehash = shortstatehash.unwrap_or(room.shortstatehash);
217
218	assemble(
219		payload,
220		public_receipts,
221		private_receipts,
222		account_data,
223		(config_hash, required_state, shortstatehash, unread_state),
224	)
225}
226
227async fn public_receipts(
228	SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
229	conn: &Connection,
230	room_id: &RoomId,
231	roomsince: u64,
232	ignored: &OnceCell<Option<IgnoredUserListEvent>>,
233) -> Result<impl Iterator<Item = Raw<AnySyncEphemeralRoomEvent>>> {
234	let mut receipts: Vec<(OwnedUserId, Raw<AnySyncEphemeralRoomEvent>)> = services
235		.read_receipt
236		.readreceipts_since_fallible(room_id, roomsince, Some(conn.next_batch))
237		.map_ok(|(user_id, _ts, event)| (user_id.to_owned(), event))
238		.try_collect()
239		.await?;
240
241	if !receipts.is_empty() {
242		let ignored = ignored
243			.get_or_try_init(async || {
244				services
245					.account_data
246					.get_global(sender_user, GlobalAccountDataEventType::IgnoredUserList)
247					.await
248					.optional()
249			})
250			.await?;
251
252		if let Some(ignored) = ignored {
253			receipts.retain(|(user_id, _)| {
254				!ignored
255					.content
256					.ignored_users
257					.contains_key(user_id)
258			});
259		}
260	}
261
262	Ok(receipts.into_iter().map(at!(1)))
263}
264
265async fn private_receipts(
266	SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
267	conn: &Connection,
268	room_id: &RoomId,
269	roomsince: u64,
270) -> Result<PrivateReadEvents> {
271	let update = services
272		.read_receipt
273		.last_privateread_update_fallible(sender_user, room_id)
274		.await?;
275
276	match update {
277		| _ if update <= roomsince => Ok(PrivateReadEvents::new()),
278		| _ if update > conn.next_batch =>
279			Err(err!(Database("Private read advanced beyond the bounded sync range."))),
280		| _ =>
281			services
282				.read_receipt
283				.private_read_get_fallible(room_id, sender_user, update)
284				.await,
285	}
286}
287
288async fn room_account_data(
289	SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
290	conn: &Connection,
291	room_id: &RoomId,
292	roomsince: u64,
293) -> Result<Vec<Raw<AnyRoomAccountDataEvent>>> {
294	services
295		.account_data
296		.changes_since_fallible(Some(room_id), sender_user, roomsince, Some(conn.next_batch))
297		.ready_try_filter_map(|event| Ok(extract_variant!(event, AnyRawAccountDataEvent::Room)))
298		.ready_try_filter(move |event| roomsince != 0 || !is_empty_account_data_event(event))
299		.try_collect()
300		.await
301}
302
303fn assemble<PublicReceipts>(
304	payload: Option<response::Room>,
305	public_receipts: PublicReceipts,
306	private_receipts: PrivateReadEvents,
307	account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
308	config: DeliveredConfig,
309) -> Result<CompleteRange, Failure>
310where
311	PublicReceipts: Iterator<Item = Raw<AnySyncEphemeralRoomEvent>>,
312{
313	let mut receipts = public_receipts.chain(private_receipts).peekable();
314	let receipts = receipts
315		.peek()
316		.is_some()
317		.then(|| pack_receipts_fallible(receipts))
318		.transpose()
319		.map_err(|error| Failure::new(Domain::ReceiptSerialization, error))?;
320
321	let config = payload.as_ref().map(|_| config);
322
323	Ok(CompleteRange { payload, config, receipts, account_data })
324}
325
326#[cfg(test)]
327mod tests {
328	use std::iter::once;
329
330	use ruma::{api::client::sync::sync_events::v5::response::Room as ResponseRoom, room_id};
331	use serde_json::{json, value::to_raw_value};
332
333	use super::*;
334
335	#[test]
336	fn malformed_receipt_withholds_the_complete_range() {
337		let room_id = room_id!("!receipt:example.com");
338		let malformed = Raw::from_json(
339			to_raw_value(&json!({"content": 5})).expect("test JSON should serialize"),
340		);
341
342		let range = assemble(
343			Some(ResponseRoom::default()),
344			vec![malformed].into_iter(),
345			PrivateReadEvents::new(),
346			Vec::new(),
347			(7, once(11).collect(), 13, Default::default()),
348		);
349
350		let error = range.expect_err("malformed receipt must fail the complete range");
351
352		assert_eq!(error.domain, Domain::ReceiptSerialization);
353		assert!(
354			!publish(room_id, Err(error))
355				.ranges
356				.contains_key(room_id)
357		);
358	}
359
360	#[test]
361	fn extension_only_range_commits_without_a_room_payload() {
362		let room_id = room_id!("!extension-only:example.com");
363		let range = assemble(
364			None,
365			Vec::new().into_iter(),
366			PrivateReadEvents::new(),
367			Vec::new(),
368			(7, once(11).collect(), 13, Default::default()),
369		);
370
371		let mut range = publish(room_id, range); // room_updates consumes the configuration.
372
373		assert_eq!(range.room_updates().collect::<Vec<_>>(), [(room_id, None)]);
374		assert!(range.into_payloads().is_empty());
375	}
376
377	#[test]
378	fn extension_outputs_are_taken_once_without_removing_the_complete_range() {
379		let room_id = room_id!("!extension-output:example.com");
380		let receipt = Raw::from_json(
381			to_raw_value(&json!({"content": {}})).expect("test receipt should serialize"),
382		);
383
384		let account_data = Raw::from_json(
385			to_raw_value(&json!({"type": "m.tag", "content": {"tags": {}}}))
386				.expect("test account data should serialize"),
387		);
388
389		let range = CompleteRange {
390			payload: None,
391			config: None,
392			receipts: Some(receipt),
393			account_data: vec![account_data],
394		};
395
396		let ranges = [(room_id.to_owned(), range)].into();
397		let mut results = Results { ranges };
398
399		assert!(results.take_receipts(room_id).is_some());
400		assert!(results.take_receipts(room_id).is_none());
401
402		let count = results
403			.take_account_data(room_id)
404			.map(|events| events.len());
405
406		assert_eq!(Some(1), count);
407		assert!(results.take_account_data(room_id).is_none());
408		assert!(results.ranges.contains_key(room_id));
409	}
410
411	fn publish(room_id: &RoomId, range: Result<CompleteRange, Failure>) -> Results {
412		let ranges = range
413			.ok()
414			.map(|range| (room_id.to_owned(), range))
415			.into_iter()
416			.collect();
417
418		Results { ranges }
419	}
420}