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#[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 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 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 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 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() .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 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 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 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}