1mod bump_stamp;
2mod heroes;
3
4#[cfg(test)]
5mod tests;
6
7use std::collections::{BTreeMap, HashSet};
8
9use futures::{
10 FutureExt, StreamExt, TryFutureExt,
11 future::{join, join3, join4},
12};
13use ruma::{
14 JsOption, MxcUri, OwnedEventId, OwnedMxcUri, RoomId, UInt, UserId,
15 api::client::sync::sync_events::{
16 UnreadNotificationsCount,
17 v5::{DisplayName, response, response::Heroes},
18 },
19 events::{
20 AnySyncStateEvent, StateEventType, TimelineEventType, room::member::MembershipState,
21 },
22 serde::Raw,
23};
24use tuwunel_core::{
25 Error, Result, at, format_small_string, is_equal_to,
26 matrix::{
27 Event, StateKey,
28 pdu::{PduCount, PduEvent, RawPduId},
29 },
30 ref_at,
31 smallstr::SmallString,
32 smallvec::SmallVec,
33 utils::{
34 BoolExt, IterStream, OptionExt, ReadyExt, TryFutureExtExt,
35 hash::sha256::{
36 Digest as Sha256Digest, delimited as sha256_delimited, hash as sha256_hash,
37 },
38 math::usize_from_ruma,
39 result::FlatOk,
40 stream::{BroadbandExt, WidebandExt},
41 },
42};
43use tuwunel_service::{
44 Services,
45 sync::{RequiredState, Room, RoomConfig},
46};
47
48use self::{bump_stamp::room_bump_stamp, heroes::calculate_heroes};
49use super::{
50 super::{load_timeline_fallible, strip_prev_state},
51 Connection, ListIds, SyncInfo, WindowRoom,
52};
53use crate::client::{annotate_membership, ignored_filter, with_membership};
54
55#[derive(Debug)]
56pub(super) enum Failure {
57 Timeline(Error),
58 Payload(Error),
59}
60
61type ThreadCounts = BTreeMap<OwnedEventId, (u64, u64)>;
62type EventTypeString = SmallString<[u8; 32]>;
63type TimelineMembers<'a> = SmallVec<[&'a str; 2]>;
64pub(super) type RoomDetails = (usize, HashSet<(StateEventType, StateKey)>);
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub(super) enum StateMode {
68 Full,
69 Delta(PduCount),
70}
71
72#[derive(Clone, Copy)]
73struct StateSelection<'a> {
74 mode: StateMode,
75 previous: Option<&'a [u64]>,
76 changed: bool,
77}
78
79#[tracing::instrument(
80 name = "room",
81 level = "debug",
82 skip_all,
83 fields(room_id, roomsince)
84)]
85pub(super) async fn handle_room(
86 sync_info: SyncInfo<'_>,
87 conn: &Connection,
88 window_room: &WindowRoom,
89 room: &Room,
90 config_changed: bool,
91 room_details: RoomDetails,
92) -> Result<response::Room, Failure> {
93 let SyncInfo {
94 services,
95 sender_user,
96 previous_connection_pos,
97 direct_rooms,
98 ..
99 } = sync_info;
100
101 let WindowRoom { lists, membership, room_id, .. } = window_room;
102 let roomsince = room.roomsince;
103
104 if matches!(*membership, Some(MembershipState::Leave | MembershipState::Ban)) {
105 return leave_or_ban_response(sync_info, conn, window_room, roomsince)
106 .map_err(Failure::Payload)
107 .await;
108 }
109
110 let is_invite = *membership == Some(MembershipState::Invite);
111
112 let encrypted = services.state_accessor.is_encrypted_room(room_id);
113
114 let (timeline_limit, required_state) = room_details;
115
116 let timeline = is_invite.is_false().then_async(|| {
117 load_timeline_fallible(
118 services,
119 sender_user,
120 room_id,
121 PduCount::Normal(roomsince),
122 Some(PduCount::from(conn.next_batch)),
123 timeline_limit,
124 )
125 });
126
127 let timeline = timeline
128 .map(Option::transpose)
129 .map_err(Failure::Timeline);
130
131 let (encrypted, timeline) = join(encrypted, timeline).await;
132
133 let (timeline_pdus, limited, last_timeline_count) =
135 timeline?.unwrap_or_else(|| (Vec::new(), false, PduCount::default()));
136
137 let limited = room_timeline_limited(timeline_limit, limited);
138
139 let prev_batch = timeline_pdus
140 .first()
141 .map(at!(0))
142 .map(PduCount::into_unsigned)
143 .as_ref()
144 .map(ToString::to_string);
145
146 let bump_stamp = room_bump_stamp(
147 services,
148 sender_user,
149 room_id,
150 PduCount::Normal(roomsince),
151 PduCount::from(conn.next_batch),
152 last_timeline_count,
153 )
154 .map_err(Failure::Timeline)
155 .await?;
156
157 let mode = state_mode(roomsince, room.required_state.is_empty());
158 let state = StateSelection {
159 mode,
160 previous: config_changed.then_some(room.required_state.as_slice()),
161 changed: state_may_have_changed(mode, last_timeline_count),
162 };
163
164 let required_state = membership_allows_required_state(membership.as_ref())
165 .and_is(state.changed || state.previous.is_some())
166 .then_some(required_state)
167 .unwrap_or_default();
168
169 let required_state = collect_required_state(
170 services,
171 sender_user,
172 room_id,
173 state,
174 &required_state,
175 &timeline_pdus,
176 encrypted,
177 );
178
179 let invite_state = is_invite.then_async(|| {
181 services
182 .state_cache
183 .invite_state(sender_user, room_id)
184 .ok()
185 });
186
187 let timeline = timeline_pdus
188 .iter()
189 .stream()
190 .filter_map(|item| ignored_filter(services, item.clone(), sender_user))
191 .wide_then(|(position, pdu)| {
192 with_membership(services, pdu, sender_user, encrypted).map(move |pdu| (position, pdu))
193 })
194 .wide_then(|(position, pdu)| {
195 services
196 .pdu_metadata
197 .bundle_aggregations(sender_user, pdu)
198 .map(move |pdu| (position, pdu))
199 })
200 .map(|(position, pdu)| (position, Event::into_format(pdu)))
201 .collect::<Vec<_>>();
202
203 let meta = room_meta_future(services, room_id);
204 let events = join3(timeline, required_state, invite_state);
205 let member_counts = member_counts_future(services, room_id);
206 let notification_counts = notification_counts_future(services, sender_user, room_id);
207 let (
208 (room_name, room_avatar),
209 (timeline, required_state, invite_state),
210 (joined_count, invited_count),
211 (highlight_count, notification_count, _last_notification_read, thread_counts),
212 ) = join4(meta, events, member_counts, notification_counts)
213 .boxed()
214 .await;
215
216 let (heroes, heroes_name, heroes_avatar) = resolve_heroes(
217 services,
218 sender_user,
219 room_id,
220 room_name.as_ref(),
221 room_avatar.as_deref(),
222 )
223 .await;
224
225 let previous_connection_pos = previous_connection_pos.filter(|_| !is_invite);
226 let (initial, num_live) =
227 room_timeline_metadata(roomsince, previous_connection_pos, &timeline);
228
229 let timeline = timeline.into_iter().map(at!(1)).collect();
230
231 Ok(response::Room {
232 initial,
233 lists: lists.clone(),
234 membership: membership.clone(),
235 name: room_name.or(heroes_name),
236 avatar: JsOption::from_option(room_avatar.or(heroes_avatar)),
237 is_dm: direct_rooms.contains(room_id).then_some(true),
238 heroes,
239 required_state,
240 invite_state: invite_state.flatten(),
241 prev_batch: prev_batch.as_deref().map(Into::into),
242 num_live,
243 limited,
244 timeline,
245 bump_stamp,
246 joined_count,
247 invited_count,
248 unread_notifications: merge_unread_notifications(
249 highlight_count,
250 notification_count,
251 &thread_counts,
252 ),
253 })
254}
255
256async fn leave_or_ban_response(
257 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
258 conn: &Connection,
259 WindowRoom { lists, membership, room_id, .. }: &WindowRoom,
260 roomsince: u64,
261) -> Result<response::Room> {
262 let member_event = services
265 .state_accessor
266 .room_state_get(room_id, &StateEventType::RoomMember, sender_user.as_str())
267 .map_ok(Event::into_format)
268 .await
269 .ok();
270
271 Ok(response::Room {
272 initial: roomsince.eq(&0).then_some(true),
273 lists: lists.clone(),
274 membership: membership.clone(),
275 prev_batch: Some(conn.next_batch.to_string().into()),
276 limited: true,
277 required_state: member_event.into_iter().collect(),
278 ..Default::default()
279 })
280}
281
282pub(super) fn merged_room_details(
283 conn: &Connection,
284 lists: &ListIds,
285 room_id: &RoomId,
286) -> RoomDetails {
287 lists
288 .iter()
289 .filter_map(|list_id| conn.lists.get(list_id))
290 .map(|list| &list.room_details)
291 .chain(conn.subscriptions.get(room_id))
292 .fold((0_usize, HashSet::new()), |(timeline_limit, mut required_state), config| {
293 required_state.extend(config.required_state.iter().cloned());
294 (timeline_limit.max(usize_from_ruma(config.timeline_limit)), required_state)
295 })
296}
297
298pub(super) fn room_config((timeline_limit, required_state): &RoomDetails) -> RoomConfig {
299 let timeline_limit = u64::try_from(*timeline_limit).expect("timeline limit must fit u64");
300 let digest = sha256_hash(timeline_limit.to_be_bytes());
301
302 required_state.iter().fold(
303 (digest_word(digest), RequiredState::new()),
304 |(hash, mut selectors), (event_type, state_key)| {
305 let entry = required_state_hash(event_type, state_key.as_str());
306
307 selectors.extend(state_key.as_str().ne("$LAZY").then_some(entry));
308
309 (hash ^ entry, selectors)
310 },
311 )
312}
313
314fn state_mode(roomsince: u64, unknown: bool) -> StateMode {
315 match (roomsince, unknown) {
316 | (0, _) | (_, true) => StateMode::Full,
317 | (roomsince, false) => StateMode::Delta(PduCount::Normal(roomsince)),
318 }
319}
320
321fn required_state_hash(event_type: &StateEventType, state_key: &str) -> u64 {
322 let event_type: EventTypeString = format_small_string!("{event_type}");
323 let digest = sha256_delimited([event_type.as_str(), state_key].into_iter());
324
325 digest_word(digest)
326}
327
328fn digest_word(digest: Sha256Digest) -> u64 {
329 u64::from_be_bytes(
330 digest[..8]
331 .try_into()
332 .expect("SHA-256 digest must contain eight bytes"),
333 )
334}
335
336pub(super) fn membership_allows_required_state(membership: Option<&MembershipState>) -> bool {
337 matches!(membership, None | Some(MembershipState::Join))
338}
339
340fn state_may_have_changed(state_mode: StateMode, last_timeline_count: PduCount) -> bool {
345 match state_mode {
346 | StateMode::Full => true,
347 | StateMode::Delta(since) => last_timeline_count > since,
348 }
349}
350
351fn room_timeline_limited(timeline_limit: usize, limited: bool) -> bool {
352 timeline_limit > 0 && limited
353}
354
355fn room_timeline_metadata<Event>(
356 roomsince: u64,
357 previous_connection_pos: Option<u64>,
358 timeline_pdus: &[(PduCount, Event)],
359) -> (Option<bool>, Option<UInt>) {
360 let initial = roomsince.eq(&0).then_some(true);
361 let num_live = previous_connection_pos
362 .map(PduCount::from)
363 .and_then(|previous_connection_pos| {
364 timeline_pdus
365 .iter()
366 .rev()
367 .map(|(position, _)| *position)
368 .take_while(|position| *position > previous_connection_pos)
369 .count()
370 .try_into()
371 .ok()
372 });
373
374 (initial, num_live)
375}
376
377async fn resolve_heroes(
378 services: &Services,
379 sender_user: &UserId,
380 room_id: &RoomId,
381 room_name: Option<&DisplayName>,
382 room_avatar: Option<&MxcUri>,
383) -> (Option<Heroes>, Option<DisplayName>, Option<OwnedMxcUri>) {
384 services
385 .config
386 .calculate_heroes
387 .then_async(|| calculate_heroes(services, sender_user, room_id, room_name, room_avatar))
388 .await
389 .unwrap_or_default()
390}
391
392fn room_meta_future<'a>(
393 services: &'a Services,
394 room_id: &'a RoomId,
395) -> impl Future<Output = (Option<DisplayName>, Option<OwnedMxcUri>)> + Send + 'a {
396 let room_name = services
397 .state_accessor
398 .get_name(room_id)
399 .map_ok(Into::into)
400 .map(Result::ok);
401
402 let room_avatar = services
403 .state_accessor
404 .get_avatar(room_id)
405 .map_ok(|content| content.url)
406 .ok()
407 .map(Option::flatten);
408
409 join(room_name, room_avatar)
410}
411
412fn member_counts_future<'a>(
413 services: &'a Services,
414 room_id: &'a RoomId,
415) -> impl Future<Output = (Option<UInt>, Option<UInt>)> + Send + 'a {
416 let joined_count = services
417 .state_cache
418 .room_joined_count(room_id)
419 .map_ok(TryInto::try_into)
420 .map_ok(Result::ok)
421 .map(FlatOk::flat_ok);
422
423 let invited_count = services
424 .state_cache
425 .room_invited_count(room_id)
426 .map_ok(TryInto::try_into)
427 .map_ok(Result::ok)
428 .map(FlatOk::flat_ok);
429
430 join(joined_count, invited_count)
431}
432
433fn notification_counts_future<'a>(
434 services: &'a Services,
435 sender_user: &'a UserId,
436 room_id: &'a RoomId,
437) -> impl Future<Output = (Option<UInt>, Option<UInt>, Result<u64>, ThreadCounts)> + Send + 'a {
438 let highlight_count = services
439 .pusher
440 .highlight_count(sender_user, room_id)
441 .map(TryInto::try_into)
442 .map(Result::ok);
443
444 let notification_count = services
445 .pusher
446 .notification_count(sender_user, room_id)
447 .map(TryInto::try_into)
448 .map(Result::ok);
449
450 let last_read_count = services
451 .pusher
452 .last_notification_read(sender_user, room_id);
453
454 let thread_counts = services
455 .pusher
456 .thread_notification_counts(sender_user, room_id);
457
458 join4(highlight_count, notification_count, last_read_count, thread_counts)
459}
460
461fn merge_unread_notifications(
463 highlight_count: Option<UInt>,
464 notification_count: Option<UInt>,
465 thread_counts: &ThreadCounts,
466) -> UnreadNotificationsCount {
467 let (thread_notifications, thread_highlights) = thread_counts
468 .values()
469 .fold((0_u64, 0_u64), |(n, h), &(notifs, hl)| {
470 (n.saturating_add(notifs), h.saturating_add(hl))
471 });
472
473 let merge = |total: u64| {
474 move |count: UInt| count.saturating_add(UInt::try_from(total).unwrap_or_default())
475 };
476
477 UnreadNotificationsCount {
478 highlight_count: highlight_count.map(merge(thread_highlights)),
479 notification_count: notification_count.map(merge(thread_notifications)),
480 }
481}
482
483async fn collect_required_state(
484 services: &Services,
485 sender_user: &UserId,
486 room_id: &RoomId,
487 selection: StateSelection<'_>,
488 required_state: &HashSet<(StateEventType, StateKey)>,
489 timeline_pdus: &[(PduCount, PduEvent)],
490 encrypted: bool,
491) -> Vec<Raw<AnySyncStateEvent>> {
492 let StateSelection { mode: state_mode, previous, changed } = selection;
493 let lazy = required_state
494 .iter()
495 .any(is_equal_to!(&(StateEventType::RoomMember, "$LAZY".into())));
496
497 let needs_since_state = required_state
498 .iter()
499 .any(|(_, state_key)| state_key != "$LAZY");
500
501 let since_state = match state_mode {
503 | StateMode::Delta(since) if changed && needs_since_state => services
504 .timeline
505 .next_shortstatehash(room_id, since)
506 .ok()
507 .await
508 .map(|shortstatehash| (since, shortstatehash)),
509 | _ => None,
510 };
511
512 let state_unchanged = !changed
514 || since_state
515 .map_async(|(_, since_shortstatehash)| {
516 services
517 .state
518 .get_room_shortstatehash(room_id)
519 .ok()
520 .map(move |current| current == Some(since_shortstatehash))
521 })
522 .await
523 .unwrap_or(false);
524
525 let timeline_senders = timeline_pdus
526 .iter()
527 .filter(|_| lazy)
528 .map(ref_at!(1))
529 .map(Event::sender)
530 .map(UserId::as_str);
531
532 let timeline_member_targets = timeline_pdus
533 .iter()
534 .filter(|_| lazy)
535 .map(ref_at!(1))
536 .filter(|event| *event.event_type() == TimelineEventType::RoomMember)
537 .filter_map(Event::state_key);
538
539 let wildcard_state = required_state
540 .iter()
541 .filter(|(_, state_key)| (!state_unchanged || previous.is_some()) && state_key == "*")
542 .stream()
543 .flat_map(|(event_type, _)| {
544 services
545 .state_accessor
546 .room_state_keys_with_ids(room_id, event_type)
547 .ready_filter_map(Result::ok)
548 .map(move |(state_key, event_id)| {
549 ((event_type.clone(), state_key), Some(event_id), false)
550 })
551 });
552
553 let mut timeline_members: TimelineMembers<'_> = timeline_senders
554 .chain(timeline_member_targets)
555 .collect();
556
557 timeline_members.sort_unstable();
558 timeline_members.dedup();
559
560 let timeline_members = timeline_members
561 .into_iter()
562 .map(|sender| (StateEventType::RoomMember, StateKey::from_str(sender)));
563
564 let in_timeline = |event: &PduEvent| {
565 timeline_pdus
566 .iter()
567 .map(ref_at!(1))
568 .map(Event::event_id)
569 .any(is_equal_to!(event.event_id()))
570 };
571
572 required_state
573 .iter()
574 .filter(|_| !state_unchanged || previous.is_some())
575 .cloned()
576 .map(|state| (state, None, false))
577 .stream()
578 .chain(wildcard_state)
579 .chain(
580 timeline_members
581 .map(|state| (state, None, true))
582 .stream(),
583 )
584 .broad_filter_map(async |(state, event_id, lazy)| {
585 let (event_type, state_key) = state;
586 let state_key: StateKey = match state_key.as_str() {
587 | "$LAZY" | "*" => return None,
588 | "$ME" => sender_user.as_str().into(),
589 | _ => state_key,
590 };
591
592 let state_mode = previous
593 .filter(|previous| {
594 !state_was_requested(previous, &event_type, state_key.as_str(), sender_user)
595 })
596 .map(|_| StateMode::Full)
597 .unwrap_or(state_mode);
598
599 if state_unchanged && !lazy && state_mode != StateMode::Full {
600 return None;
601 }
602
603 let event_id = match event_id {
604 | Some(event_id) => event_id,
605 | None =>
606 services
607 .state_accessor
608 .room_state_get_id(room_id, &event_type, &state_key)
609 .ok()
610 .await?,
611 };
612
613 let pdu_id = services.timeline.get_pdu_id(&event_id).await.ok();
614 let count = pdu_id.map(RawPduId::pdu_count);
615 let same_at_since = since_state
616 .filter(|(since, _)| !lazy && count.is_some_and(|count| count <= *since))
617 .map_async(|(_, shortstatehash)| {
618 services
619 .state_accessor
620 .state_get_id(shortstatehash, &event_type, &state_key)
621 .ok()
622 })
623 .await
624 .flatten()
625 .is_some_and(|previous_event_id| previous_event_id == event_id);
626
627 let pdu_id =
628 state_is_required(state_mode, count, lazy, same_at_since).then_some(pdu_id)?;
629
630 let mut pdu = match pdu_id {
631 | None => services
632 .timeline
633 .get_outlier_pdu(&event_id)
634 .await
635 .ok()?,
636 | Some(pdu_id) => services
637 .timeline
638 .get_pdu_from_id(&pdu_id)
639 .or_else(|_| services.timeline.get_outlier_pdu(&event_id))
640 .await
641 .ok()?,
642 };
643
644 annotate_membership(services, &mut pdu, sender_user, encrypted).await;
645
646 let pdu = strip_prev_state(pdu, sender_user, in_timeline);
647
648 Some(Event::into_format(pdu))
649 })
650 .collect()
651 .await
652}
653
654fn state_was_requested(
655 previous: &[u64],
656 event_type: &StateEventType,
657 state_key: &str,
658 sender_user: &UserId,
659) -> bool {
660 let contains = |key| previous.contains(&required_state_hash(event_type, key));
661
662 contains("*") || contains(state_key) || (state_key == sender_user.as_str() && contains("$ME"))
663}
664
665fn state_is_required(
666 state_mode: StateMode,
667 count: Option<PduCount>,
668 lazy: bool,
669 same_at_since: bool,
670) -> bool {
671 lazy || match state_mode {
672 | StateMode::Full => true,
673 | StateMode::Delta(since) => count.is_none_or(|count| count > since) || !same_at_since,
674 }
675}