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 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#[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 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 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 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
298fn 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
335fn 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(); 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
372async 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#[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
432fn 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}