Skip to main content

tuwunel_api_client/sync/v3/
sync_events.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use axum::extract::State;
4use futures::{
5	FutureExt, StreamExt, TryFutureExt,
6	future::{join, join3, join4, join5},
7};
8use ruma::{
9	DeviceId, OwnedRoomId, OwnedUserId, UserId,
10	api::client::{
11		filter::FilterDefinition,
12		sync::{
13			sync_events,
14			sync_events::{
15				DeviceLists,
16				v3::{
17					Filter, GlobalAccountData, InviteState, InvitedRoom, JoinedRoom, KnockState,
18					KnockedRoom, Presence, Response, Rooms, ToDevice,
19				},
20			},
21		},
22	},
23	events::{
24		AnyGlobalAccountDataEvent, AnyRawAccountDataEvent,
25		presence::{PresenceEvent, PresenceEventContent},
26	},
27	serde::Raw,
28};
29use tokio::time::timeout_at;
30use tuwunel_core::{
31	Result, at,
32	debug::INFO_SPAN_LEVEL,
33	error::inspect_log,
34	extract_variant, trace,
35	utils::{
36		BoolExt, IterStream, ReadyExt, TryFutureExtExt,
37		option::OptionExt,
38		stream::{BroadbandExt, Tools},
39	},
40};
41use tuwunel_matrix::event::Matches;
42use tuwunel_service::{Services, presence::Ping, users::InviteFilter};
43
44use super::{
45	super::{invite_permitted, profiles::collect as collect_profiles, share_encrypted_room},
46	StateAfter,
47	joined::load_room as load_joined_room,
48	left::collect_rooms as collect_left_rooms,
49	log_future_since,
50};
51use crate::{
52	ClientIp, Ruma, is_empty_account_data_event,
53	utils::{normalize_profile_fields, sync_deadline},
54};
55
56struct Params<'a> {
57	services: &'a Services,
58	sender_user: &'a UserId,
59	sender_device: Option<&'a DeviceId>,
60	since: Option<u64>,
61	next_batch: u64,
62	full_state: bool,
63	state_after: StateAfter,
64	filter: &'a FilterDefinition,
65}
66
67type PresenceUpdates = HashMap<OwnedUserId, PresenceEventContent>;
68
69/// # `GET /_matrix/client/r0/sync`
70///
71/// Synchronize the client's state with the latest state on the server.
72///
73/// - This endpoint takes a `since` parameter which should be the `next_batch`
74///   value from a previous request for incremental syncs.
75///
76/// Calling this endpoint without a `since` parameter returns:
77/// - Some of the most recent events of each timeline
78/// - Notification counts for each room
79/// - Joined and invited member counts, heroes
80/// - All state events
81///
82/// Calling this endpoint with a `since` parameter from a previous `next_batch`
83/// returns: For joined rooms:
84/// - Some of the most recent events of each timeline that happened after since
85/// - If user joined the room after since: All state events (unless lazy loading
86///   is activated) and all device list updates in that room
87/// - If the user was already in the room: A list of all events that are in the
88///   state now, but were not in the state at `since`
89/// - If the state we send contains a member event: Joined and invited member
90///   counts, heroes
91/// - Device list updates that happened after `since`
92/// - If there are events in the timeline we send or the user send updated his
93///   read mark: Notification counts
94/// - EDUs that are active now (read receipts, typing updates, presence)
95/// - TODO: Allow multiple sync streams to support Pantalaimon
96///
97/// For invited rooms:
98/// - If the user was invited after `since`: A subset of the state of the room
99///   at the point of the invite
100///
101/// For left rooms:
102/// - If the user left after `since`: `prev_batch` token, empty state (TODO:
103///   subset of the state at the point of the leave)
104#[tracing::instrument(
105	target = "tuwunel_api_client::sync::v3",
106	name = "sync",
107	level = "debug",
108	skip_all,
109	fields(
110		user_id = %body.sender_user(),
111		device_id = %body.sender_device.as_deref().map_or("<no device>", |x| x.as_str()),
112    )
113)]
114pub(crate) async fn sync_events_route(
115	State(services): State<crate::State>,
116	ClientIp(client): ClientIp,
117	body: Ruma<sync_events::v3::Request>,
118) -> Result<Response> {
119	let sender_user = body.sender_user();
120	let sender_device = body.sender_device.as_deref();
121
122	let filter = body
123		.body
124		.filter
125		.as_ref()
126		.map_async(async |filter| match filter {
127			| Filter::FilterDefinition(filter) => filter.clone(),
128			| Filter::FilterId(filter_id) => services
129				.users
130				.get_filter(sender_user, filter_id)
131				.await
132				.unwrap_or_default(),
133		});
134
135	let filter = filter.map(Option::unwrap_or_default);
136	let full_state = body.body.full_state;
137	let set_presence = &body.body.set_presence;
138	let state_after =
139		StateAfter::from((body.body.use_state_after, body.body.use_state_after_unstable));
140
141	let ping = Ping {
142		device_id: body.sender_device.as_deref(),
143		client_ip: Some(client),
144		new_state: Some(set_presence),
145		appservice: body.appservice_info.as_ref(),
146	};
147
148	let ping_presence = services
149		.presence
150		.maybe_ping_presence(sender_user, ping)
151		.inspect_err(inspect_log)
152		.ok();
153
154	// Record user as actively syncing for push suppression heuristic.
155	let note_sync = services
156		.presence
157		.note_sync(sender_user, body.appservice_info.as_ref());
158
159	let (filter, ..) = join3(filter, ping_presence, note_sync).await;
160
161	services
162		.profile
163		.check_requested_fields(&filter.profile_fields.ids)?;
164
165	let filter = normalize_profile_fields(filter);
166
167	let mut since = body
168		.body
169		.since
170		.as_deref()
171		.map(|since| since.parse().unwrap_or(0));
172
173	let stop_at = sync_deadline(&services, body.body.timeout);
174
175	loop {
176		let watch_rooms = services
177			.state_cache
178			.rooms_joined(sender_user)
179			.chain(services.state_cache.rooms_invited(sender_user));
180
181		let watchers = services
182			.sync
183			.watch(sender_user, sender_device, watch_rooms)
184			.await;
185
186		let next_batch = services.globals.wait_pending().await?;
187		if since.is_some_and(|since| since > next_batch) {
188			log_future_since(since, next_batch);
189			since = Some(next_batch);
190		}
191
192		if since.is_none_or(|since| since < next_batch) || full_state {
193			let response = build_sync_events(Params {
194				services: &services,
195				sender_user,
196				sender_device,
197				since,
198				next_batch,
199				full_state,
200				state_after,
201				filter: &filter,
202			})
203			.await?;
204
205			let empty = response.rooms.is_empty()
206				&& response.presence.is_empty()
207				&& response.account_data.is_empty()
208				&& response.device_lists.is_empty()
209				&& response.to_device.is_empty()
210				&& response.users.is_empty();
211
212			if !empty || full_state {
213				return Ok(response);
214			}
215		}
216
217		// Wait for activity
218		if timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
219			let response =
220				build_empty_response(&services, sender_user, sender_device, next_batch).await;
221
222			trace!(target: "tuwunel_api_client::sync::v3", ?since, next_batch, "empty response");
223			return Ok(response);
224		}
225
226		trace!(target: "tuwunel_api_client::sync::v3",
227			?since,
228			last_batch = ?next_batch,
229			count = ?services.globals.pending_count(),
230			stop_at = ?stop_at,
231			"notified by watcher"
232		);
233
234		since = Some(next_batch);
235	}
236}
237
238async fn build_empty_response(
239	services: &Services,
240	sender_user: &UserId,
241	sender_device: Option<&DeviceId>,
242	next_batch: u64,
243) -> Response {
244	let device_one_time_keys_count = sender_device.map_async(|sender_device| {
245		services
246			.users
247			.count_one_time_keys(sender_user, sender_device)
248	});
249
250	let device_unused_fallback_key_types = sender_device.map_async(|sender_device| {
251		services
252			.users
253			.unused_fallback_key_algorithms(sender_user, sender_device)
254			.collect::<Vec<_>>()
255	});
256
257	let (device_one_time_keys_count, device_unused_fallback_key_types) =
258		join(device_one_time_keys_count, device_unused_fallback_key_types).await;
259
260	Response {
261		device_one_time_keys_count: device_one_time_keys_count.unwrap_or_default(),
262		device_unused_fallback_key_types,
263		..Response::new(next_batch.to_string())
264	}
265}
266
267#[tracing::instrument(
268	target = "tuwunel_api_client::sync::v3",
269	name = "build",
270	level = INFO_SPAN_LEVEL,
271	skip_all,
272	fields(
273		?since,
274		%next_batch,
275		count = ?services.globals.pending_count(),
276    )
277)]
278async fn build_sync_events(
279	Params {
280		services,
281		sender_user,
282		sender_device,
283		since,
284		next_batch,
285		full_state,
286		state_after,
287		filter,
288	}: Params<'_>,
289) -> Result<Response> {
290	let profile_since = since;
291	let since = since.unwrap_or(0);
292
293	// MSC4155: a stored invite whose sender the recipient ignores or blocks is
294	// withheld here, and relaxing the configuration re-exposes it.
295	let invite_filter = services.users.invite_filter(sender_user).await;
296
297	let joined_rooms = collect_joined_rooms(
298		services,
299		sender_user,
300		sender_device,
301		since,
302		next_batch,
303		full_state,
304		state_after,
305		filter,
306	);
307
308	let left_rooms = collect_left_rooms(
309		services,
310		sender_user,
311		since,
312		next_batch,
313		full_state,
314		state_after,
315		filter,
316	);
317
318	let invited_rooms =
319		collect_invited_rooms(services, sender_user, since, next_batch, filter, &invite_filter);
320
321	let knocked_rooms = collect_knocked_rooms(services, sender_user, since, next_batch, filter);
322
323	let presence_updates = services
324		.config
325		.presence
326		.allow_local
327		.then_async(|| {
328			process_presence_updates(services, since, next_batch, sender_user, filter)
329		});
330
331	let account_data = collect_global_account_data(services, sender_user, since, next_batch);
332
333	let keys_changed = services
334		.users
335		.keys_changed(sender_user, since, Some(next_batch))
336		.map(ToOwned::to_owned)
337		.collect::<HashSet<_>>();
338
339	let to_device_events = sender_device.map_async(|sender_device| {
340		services
341			.users
342			.get_to_device_events(sender_user, sender_device, Some(since), Some(next_batch))
343			.map(at!(1))
344			.collect::<Vec<_>>()
345	});
346
347	let device_one_time_keys_count = sender_device.map_async(|sender_device| {
348		services
349			.users
350			.count_one_time_keys(sender_user, sender_device)
351	});
352
353	let device_unused_fallback_key_types = sender_device.map_async(|sender_device| {
354		services
355			.users
356			.unused_fallback_key_algorithms(sender_user, sender_device)
357			.collect::<Vec<_>>()
358	});
359
360	// Remove all to-device events the device received *last time*
361	let remove_to_device_events = sender_device.map_async(|sender_device| {
362		services
363			.users
364			.remove_to_device_events(sender_user, sender_device, since)
365	});
366
367	let (
368		account_data,
369		keys_changed,
370		presence_updates,
371		(_, to_device_events, device_one_time_keys_count, device_unused_fallback_key_types),
372		(
373			(joined_rooms, mut device_list_updates, left_encrypted_users),
374			left_rooms,
375			invited_rooms,
376			knocked_rooms,
377		),
378	) = join5(
379		account_data,
380		keys_changed,
381		presence_updates,
382		join4(
383			remove_to_device_events,
384			to_device_events,
385			device_one_time_keys_count,
386			device_unused_fallback_key_types,
387		),
388		join4(joined_rooms, left_rooms, invited_rooms, knocked_rooms),
389	)
390	.boxed() // size firewall
391	.await;
392
393	device_list_updates.extend(keys_changed);
394
395	let rooms = Rooms {
396		leave: left_rooms,
397		join: joined_rooms,
398		invite: invited_rooms,
399		knock: knocked_rooms,
400	};
401
402	// The profile reads and the left-device fan-out both pend on the pool.
403	let (device_list_left, users) = join(
404		collect_device_list_left(services, sender_user, left_encrypted_users),
405		collect_profiles(services, sender_user, profile_since, next_batch, filter, &rooms),
406	)
407	.await;
408
409	let presence_events = build_presence_events(presence_updates);
410	let device_lists = DeviceLists {
411		left: device_list_left,
412		changed: device_list_updates.into_iter().collect(),
413	};
414
415	let to_device = ToDevice {
416		events: to_device_events.unwrap_or_default(),
417	};
418
419	Ok(Response {
420		account_data: GlobalAccountData { events: account_data },
421		device_lists,
422		device_one_time_keys_count: device_one_time_keys_count.unwrap_or_default(),
423		device_unused_fallback_key_types,
424		next_batch: next_batch.to_string(),
425		presence: Presence { events: presence_events },
426		rooms,
427		to_device,
428		users: users?,
429	})
430}
431
432#[expect(clippy::too_many_arguments)]
433fn collect_joined_rooms<'a>(
434	services: &'a Services,
435	sender_user: &'a UserId,
436	sender_device: Option<&'a DeviceId>,
437	since: u64,
438	next_batch: u64,
439	full_state: bool,
440	state_after: StateAfter,
441	filter: &'a FilterDefinition,
442) -> impl Future<
443	Output = (BTreeMap<OwnedRoomId, JoinedRoom>, HashSet<OwnedUserId>, HashSet<OwnedUserId>),
444> + Send
445+ 'a {
446	services
447		.state_cache
448		.rooms_joined(sender_user)
449		.ready_filter(|&room_id| filter.room.matches(room_id))
450		.map(ToOwned::to_owned)
451		.broad_filter_map(move |room_id| {
452			load_joined_room(
453				services,
454				sender_user,
455				sender_device,
456				room_id.clone(),
457				since,
458				next_batch,
459				full_state,
460				state_after,
461				filter,
462			)
463			.map_ok(move |(joined_room, dlu, jeu)| (room_id, joined_room, dlu, jeu))
464			.ok()
465		})
466		.ready_fold(
467			(BTreeMap::new(), HashSet::new(), HashSet::new()),
468			|(mut joined_rooms, mut device_list_updates, mut left_encrypted_users),
469			 (room_id, joined_room, dlu, leu)| {
470				device_list_updates.extend(dlu);
471				left_encrypted_users.extend(leu);
472				if !joined_room.is_empty() {
473					joined_rooms.insert(room_id, joined_room);
474				}
475
476				(joined_rooms, device_list_updates, left_encrypted_users)
477			},
478		)
479}
480
481async fn collect_invited_rooms<'a>(
482	services: &'a Services,
483	sender_user: &'a UserId,
484	since: u64,
485	next_batch: u64,
486	filter: &'a FilterDefinition,
487	invite_filter: &'a InviteFilter,
488) -> BTreeMap<OwnedRoomId, InvitedRoom> {
489	services
490		.state_cache
491		.rooms_invited_state(sender_user)
492		.ready_filter(|(room_id, _)| filter.room.matches(room_id))
493		.ready_filter(|(room_id, invite_state)| {
494			invite_permitted(sender_user, room_id, invite_filter, invite_state)
495		})
496		.fold_default(async |mut invited_rooms: BTreeMap<_, _>, (room_id, invite_state)| {
497			let invite_count = services
498				.state_cache
499				.get_invite_count(&room_id, sender_user)
500				.await
501				.ok();
502
503			// Invited before last sync
504			if Some(since) >= invite_count || Some(next_batch) < invite_count {
505				return invited_rooms;
506			}
507
508			let invited_room = InvitedRoom {
509				invite_state: InviteState { events: invite_state },
510			};
511
512			invited_rooms.insert(room_id, invited_room);
513			invited_rooms
514		})
515		.await
516}
517
518async fn collect_knocked_rooms<'a>(
519	services: &'a Services,
520	sender_user: &'a UserId,
521	since: u64,
522	next_batch: u64,
523	filter: &'a FilterDefinition,
524) -> BTreeMap<OwnedRoomId, KnockedRoom> {
525	services
526		.state_cache
527		.rooms_knocked_state(sender_user)
528		.ready_filter(|(room_id, _)| filter.room.matches(room_id))
529		.fold_default(async |mut knocked_rooms: BTreeMap<_, _>, (room_id, knock_state)| {
530			let knock_count = services
531				.state_cache
532				.get_knock_count(&room_id, sender_user)
533				.await
534				.ok();
535
536			// Knocked before last sync; or after the cutoff for this sync
537			if Some(since) >= knock_count || Some(next_batch) < knock_count {
538				return knocked_rooms;
539			}
540
541			let knocked_room = KnockedRoom {
542				knock_state: KnockState { events: knock_state },
543			};
544
545			knocked_rooms.insert(room_id, knocked_room);
546			knocked_rooms
547		})
548		.await
549}
550
551fn collect_global_account_data<'a>(
552	services: &'a Services,
553	sender_user: &'a UserId,
554	since: u64,
555	next_batch: u64,
556) -> impl Future<Output = Vec<Raw<AnyGlobalAccountDataEvent>>> + Send + 'a {
557	services
558		.account_data
559		.changes_since(None, sender_user, since, Some(next_batch))
560		.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Global))
561		.ready_filter(move |e| since != 0 || !is_empty_account_data_event(e))
562		.collect()
563}
564
565fn collect_device_list_left<'a>(
566	services: &'a Services,
567	sender_user: &'a UserId,
568	left_encrypted_users: HashSet<OwnedUserId>,
569) -> impl Future<Output = Vec<OwnedUserId>> + Send + 'a {
570	left_encrypted_users
571		.into_iter()
572		.stream()
573		.broad_filter_map(async |user_id: OwnedUserId| {
574			share_encrypted_room(services, sender_user, &user_id)
575				.await
576				.is_false()
577				.then_some(user_id)
578		})
579		.collect()
580}
581
582fn build_presence_events(presence_updates: Option<PresenceUpdates>) -> Vec<Raw<PresenceEvent>> {
583	presence_updates
584		.into_iter()
585		.flat_map(IntoIterator::into_iter)
586		.map(|(sender, content)| PresenceEvent { content, sender })
587		.map(|ref event| Raw::new(event))
588		.filter_map(Result::ok)
589		.collect()
590}
591
592#[tracing::instrument(
593	target = "tuwunel_api_client::sync::v3",
594	name = "presence",
595	level = "debug",
596	skip_all
597)]
598async fn process_presence_updates(
599	services: &Services,
600	since: u64,
601	next_batch: u64,
602	syncing_user: &UserId,
603	filter: &FilterDefinition,
604) -> PresenceUpdates {
605	services
606		.presence
607		.presence_since(since, Some(next_batch))
608		.ready_filter(|(user_id, ..)| filter.presence.matches(user_id))
609		.filter(|(user_id, ..)| {
610			services
611				.state_cache
612				.user_sees_user(syncing_user, user_id)
613		})
614		.filter_map(|(user_id, _, presence_bytes)| {
615			services
616				.presence
617				.from_json_bytes_to_event(presence_bytes, user_id)
618				.map_ok(move |event| (user_id, event))
619				.ok()
620		})
621		.map(|(user_id, event)| (user_id.to_owned(), event.content))
622		.collect()
623		.await
624}