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