Skip to main content

tuwunel_api_client/sync/
v5.rs

1mod extensions;
2mod filter;
3mod range;
4mod rooms;
5mod selector;
6
7#[cfg(test)]
8mod tests;
9
10use std::{
11	collections::{BTreeMap, BTreeSet},
12	fmt::Debug,
13	iter::{once, repeat},
14	mem::take,
15	sync::Arc,
16};
17
18use axum::extract::{Extension, State};
19use futures::{TryFutureExt, future::join};
20use ruma::{
21	DeviceId, OwnedRoomId, RoomId, UserId,
22	api::client::sync::sync_events::v5::{ListId, Request, Response, response},
23	events::{AnyStrippedStateEvent, StateEventType, room::member::MembershipState},
24	serde::Raw,
25};
26use serde::Deserialize;
27use tokio::{sync::Notify, time::timeout_at};
28use tuwunel_core::{
29	Err, Result, debug,
30	debug::INFO_SPAN_LEVEL,
31	debug_warn,
32	error::inspect_log,
33	smallvec::SmallVec,
34	trace,
35	utils::{
36		TryFutureExtExt,
37		result::{FlatOk, NotFound},
38		time::millis,
39	},
40	warn,
41};
42use tuwunel_service::{
43	Services,
44	presence::Ping,
45	rooms::state_cache::StrippedRoomState,
46	sync::{Connection, ConnectionKey, ConnectionVal, into_connection_key},
47};
48
49use self::{
50	extensions::{
51		acknowledge_to_device, apply_profiles, apply_ranges, check_to_device,
52		handle as handle_extensions, to_device_pending,
53	},
54	range::collect as collect_ranges,
55	selector::selector,
56};
57use super::{joined_since, share_encrypted_room};
58use crate::{ClientIp, Ruma, utils::poll_deadline};
59
60#[derive(Copy, Clone)]
61struct SyncInfo<'a> {
62	services: &'a Services,
63	sender_user: &'a UserId,
64	sender_device: Option<&'a DeviceId>,
65	previous_connection_pos: Option<u64>,
66
67	/// The rooms the user calls direct, read once for the whole pass.
68	///
69	/// Both the list filters and every room payload answer `is_dm` from this.
70	direct_rooms: &'a BTreeSet<OwnedRoomId>,
71}
72
73#[derive(Clone, Debug)]
74struct WindowRoom {
75	room_id: OwnedRoomId,
76	membership: Option<MembershipState>,
77	forgotten: bool,
78	lists: ListIds,
79	event_count: u64,
80	payload_count: u64,
81}
82
83impl WindowRoom {
84	#[inline]
85	fn payload_is_fresh(&self, roomsince: u64) -> bool {
86		roomsince == 0 || self.payload_count > roomsince
87	}
88}
89
90type Window = BTreeMap<OwnedRoomId, WindowRoom>;
91type ResponseLists = BTreeMap<ListId, response::List>;
92type ListIds = SmallVec<[ListId; 1]>;
93
94/// `POST /_matrix/client/unstable/org.matrix.simplified_msc3575/sync`
95/// ([MSC4186])
96///
97/// A simplified version of sliding sync ([MSC3575]).
98///
99/// Get all new events in a sliding window of rooms since the last sync or a
100/// given point in time.
101///
102/// [MSC3575]: https://github.com/matrix-org/matrix-spec-proposals/pull/3575
103/// [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
104#[tracing::instrument(
105	name = "sync",
106	level = INFO_SPAN_LEVEL,
107	skip_all,
108	fields(
109		user_id = %body.sender_user().localpart(),
110		device_id = %body.sender_device.as_deref().map_or("<no device>", |x| x.as_str()),
111		conn_id = ?body.body.conn_id.clone().unwrap_or_default(),
112		since = ?body.body.pos.clone().unwrap_or_default(),
113	)
114)]
115pub(crate) async fn sync_events_v5_route(
116	Extension(interrupted): Extension<Arc<Notify>>,
117	ClientIp(client): ClientIp,
118	State(ref services): State<crate::State>,
119	body: Ruma<Request>,
120) -> Result<Response> {
121	let sender_user = body.sender_user();
122	let sender_device = body.sender_device.as_deref();
123	let request = &body.body;
124
125	if let Some(fields) = request.extensions.profiles.fields.as_deref() {
126		services.profile.check_requested_fields(fields)?;
127	}
128
129	check_to_device(&request.extensions.to_device)?;
130
131	let since = request
132		.pos
133		.as_ref()
134		.and_then(|string| string.parse().ok())
135		.unwrap_or(0);
136
137	let timeout = poll_timeout(request, services.config.client.sync.timeout_max);
138
139	let conn_key = into_connection_key(sender_user, sender_device, request.conn_id.as_deref());
140	let conn_val = services
141		.sync
142		.load_or_init_connection(&conn_key)
143		.await?;
144
145	let conn = conn_val.lock();
146	let ping = Ping {
147		device_id: sender_device,
148		client_ip: Some(client),
149		new_state: Some(&request.set_presence),
150		appservice: body.appservice_info.as_ref(),
151	};
152
153	let ping_presence = services
154		.presence
155		.maybe_ping_presence(sender_user, ping)
156		.inspect_err(inspect_log)
157		.ok();
158
159	let (mut conn, _) = join(conn, ping_presence).await;
160
161	if since != 0 {
162		check_profile_selection(services, request, &conn)?;
163	}
164
165	if since != 0 && conn.next_batch == 0 {
166		return Err!(Request(UnknownPos(warn!("Connection lost; restarting sync stream."))));
167	}
168
169	if since == 0 {
170		*conn = Connection::default();
171		services
172			.sync
173			.store_connection(&conn_key, &conn_val, &conn)
174			.await?;
175
176		debug_warn!(?conn_key, "Client cleared cache and reloaded.");
177	}
178
179	let advancing = since == conn.next_batch;
180	let retarding = since != 0 && since <= conn.globalsince;
181	if !advancing && !retarding {
182		return Err!(Request(UnknownPos(warn!(
183			"Requesting unknown or invalid stream position."
184		))));
185	}
186
187	debug_assert!(
188		advancing || retarding,
189		"Request should either be advancing or replaying the since token."
190	);
191
192	// Update parameters regardless of replay or advance
193	conn.next_batch = services.globals.wait_pending().await?;
194	conn.globalsince = since.min(conn.next_batch);
195	let config_changed = conn.update_cache(request);
196
197	acknowledge_to_device(services, sender_user, sender_device, &conn).await?;
198
199	let caught_up = conn.globalsince == conn.next_batch;
200
201	// Caught-up connections run no pass, so config changes, owed profiles or to-device messages
202	// after `since` need a reserved position.
203	let needs_pass = config_changed
204		|| conn.own_profile_owed()
205		|| caught_up && to_device_pending(services, sender_user, sender_device, &conn).await?;
206
207	if config_change_needs_position(needs_pass, advancing, caught_up, since) {
208		// The permit publishes the reserved position when it retires on drop.
209		drop(services.globals.next_count());
210	}
211
212	conn.update_rooms_prologue(retarding.then_some(since));
213
214	let mut response = Response {
215		txn_id: request.txn_id.clone(),
216		lists: Default::default(),
217		pos: Default::default(),
218		rooms: Default::default(),
219		extensions: Default::default(),
220	};
221
222	let stop_at = poll_deadline(timeout);
223
224	for woken in once(false).chain(repeat(true)) {
225		debug_assert!(
226			conn.globalsince <= conn.next_batch,
227			"since should not be greater than next_batch."
228		);
229
230		let window;
231		let watchers = services
232			.sync
233			.watch(sender_user, sender_device, services.state_cache.rooms_joined(sender_user))
234			.await;
235
236		conn.next_batch = services.globals.wait_pending().await?;
237		let direct_rooms = services
238			.account_data
239			.direct_rooms(sender_user)
240			.await;
241
242		let sync_info = SyncInfo {
243			services,
244			sender_user,
245			sender_device,
246			previous_connection_pos: since.ne(&0).then_some(since),
247			direct_rooms: &direct_rooms,
248		};
249
250		(window, response.lists) = selector(&mut conn, sync_info).await;
251
252		if conn.globalsince < conn.next_batch {
253			let ranges = collect_ranges(sync_info, &conn, &window);
254			let extensions = handle_extensions(sync_info, &conn, &window);
255			let (mut ranges, extensions) = join(ranges, extensions).await;
256
257			let mut extensions = extensions?;
258
259			apply_profiles(sync_info, &conn, &window, &ranges, &mut extensions).await?;
260			apply_ranges(&conn, &window, &mut ranges, &mut extensions);
261			conn.update_rooms_epilogue(since, ranges.room_updates());
262			conn.update_profiles_epilogue();
263			response.rooms = ranges.into_payloads();
264			let keys_answer = woken || extensions.keys_changed();
265
266			response.extensions = extensions.into_response(&response.rooms);
267
268			if !is_empty_response(&mut response, keys_answer) {
269				return respond(services, (&conn_key, &conn_val), &conn, response).await;
270			}
271		}
272
273		let waiter = async || {
274			tokio::select! {
275				() = interrupted.notified() => true,
276				watch = timeout_at(stop_at, watchers) => watch.is_err(),
277			}
278		};
279
280		if timeout == 0 || services.server.is_stopping() || waiter().await {
281			return respond(services, (&conn_key, &conn_val), &conn, response).await;
282		}
283
284		debug!(
285			?timeout,
286			last_since = conn.globalsince,
287			last_batch = conn.next_batch,
288			pend_count = ?services.globals.pending_count(),
289			"notified by watcher"
290		);
291
292		conn.globalsince = conn.next_batch;
293	}
294
295	unreachable!("a long poll has no last pass")
296}
297
298/// How long a request may long-poll, in milliseconds.
299///
300/// Per MSC4186 the timeout of a request without `pos` is ignored, so opening
301/// a connection answers after its first pass.
302fn poll_timeout(request: &Request, max: u64) -> u64 {
303	request
304		.timeout
305		.filter(|_| request.pos.is_some())
306		.and_then(millis)
307		.map(|timeout| timeout.min(max))
308		.unwrap_or(0)
309}
310
311fn check_profile_selection(services: &Services, request: &Request, conn: &Connection) -> Result {
312	let fields = request
313		.extensions
314		.profiles
315		.fields
316		.as_deref()
317		.or(conn.extensions.profiles.fields.as_deref());
318
319	if let Some(fields) = fields {
320		services.profile.check_requested_fields(fields)?;
321	}
322
323	Ok(())
324}
325
326fn config_change_needs_position(
327	config_changed: bool,
328	advancing: bool,
329	caught_up: bool,
330	since: u64,
331) -> bool {
332	config_changed && advancing && caught_up && since != 0
333}
334
335/// Whether a pass has nothing to answer the client with.
336///
337/// The key counts and fallback types are current state; they answer a pass the
338/// user's watchers woke or one where the counts changed since `pos`.
339/// A woken answer lets matrix-rust-sdk back up its room keys.
340/// Both fields remain in the response whether or not they answer the pass.
341fn is_empty_response(response: &mut Response, keys_answer: bool) -> bool {
342	let key_counts = take(
343		&mut response
344			.extensions
345			.e2ee
346			.device_one_time_keys_count,
347	);
348
349	let fallback_key_types = response
350		.extensions
351		.e2ee
352		.device_unused_fallback_key_types
353		.take(); // Extensions::is_empty cannot skip the key fields
354
355	let empty = response.extensions.is_empty()
356		&& response.rooms.is_empty()
357		&& (!keys_answer || (key_counts.is_empty() && fallback_key_types.is_none()));
358
359	response
360		.extensions
361		.e2ee
362		.device_one_time_keys_count = key_counts;
363
364	response
365		.extensions
366		.e2ee
367		.device_unused_fallback_key_types = fallback_key_types;
368
369	empty
370}
371
372/// Positions a response at the connection's next batch and stores the
373/// connection.
374///
375/// The connection is stored as this response leaves it, so the client's next
376/// request resumes from the position the response carries.
377async fn respond(
378	services: &Services,
379	(key, loaded): (&ConnectionKey, &ConnectionVal),
380	conn: &Connection,
381	response: Response,
382) -> Result<Response> {
383	let response = Response {
384		pos: conn.next_batch.to_string().into(),
385		..response
386	};
387
388	trace!(conn.globalsince, conn.next_batch, ?response, "Responding");
389
390	services
391		.sync
392		.store_connection(key, loaded, conn)
393		.await?;
394
395	Ok(response)
396}
397
398/// The stripped state MSC4186 sends for an invited or knocked room.
399///
400/// For a knocked room this is the summary stored with the knock, ending with
401/// the user's own knock event, so a client can present a room this server is
402/// not in. An empty knock summary is omitted, since a client reading the
403/// user's membership from it would take the room for an invite.
404#[tracing::instrument(level = "trace", skip_all)]
405async fn stripped_state(
406	services: &Services,
407	sender_user: &UserId,
408	room_id: &RoomId,
409	membership: Option<&MembershipState>,
410) -> StrippedRoomState {
411	let membership = membership?;
412	let state_cache = &services.state_cache;
413	let state = match membership {
414		| MembershipState::Invite =>
415			state_cache
416				.invite_state(sender_user, room_id)
417				.await,
418		| MembershipState::Knock =>
419			state_cache
420				.knock_state(sender_user, room_id)
421				.await,
422		| _ => return None,
423	};
424
425	state
426		.present()
427		.inspect_err(|error| warn!(%room_id, %error, "Unreadable stripped room state"))
428		.flat_ok()
429		.filter(|state| !state.is_empty() || *membership == MembershipState::Invite)
430}
431
432/// The content of the stripped state event with this type and an empty state
433/// key.
434///
435/// The first match wins; an event whose type or state key does not parse is
436/// skipped.
437fn stripped_content<'a, T>(
438	state: &'a [Raw<AnyStrippedStateEvent>],
439	kind: &StateEventType,
440) -> Option<T>
441where
442	T: Deserialize<'a>,
443{
444	state
445		.iter()
446		.filter(|event| {
447			event
448				.get_field("type")
449				.flat_ok()
450				.is_some_and(|event_type: StateEventType| event_type == *kind)
451		})
452		.find(|event| {
453			event
454				.get_field("state_key")
455				.flat_ok()
456				.is_some_and(str::is_empty)
457		})?
458		.get_field("content")
459		.flat_ok()
460}
461
462#[inline]
463fn departed(membership: Option<&MembershipState>) -> bool {
464	matches!(membership, Some(MembershipState::Leave | MembershipState::Ban))
465}