1mod bump_stamp;
2mod heroes;
3
4use std::collections::{BTreeMap, HashSet};
5
6use futures::{
7 FutureExt, StreamExt, TryFutureExt,
8 future::{join, join3, join4},
9};
10use ruma::{
11 JsOption, MxcUri, OwnedEventId, OwnedMxcUri, RoomId, UInt, UserId,
12 api::client::sync::sync_events::{
13 UnreadNotificationsCount,
14 v5::{DisplayName, response, response::Heroes},
15 },
16 events::{
17 AnySyncStateEvent, StateEventType, TimelineEventType, room::member::MembershipState,
18 },
19 serde::Raw,
20};
21use tuwunel_core::{
22 Error, Result, at, is_equal_to,
23 itertools::Itertools,
24 matrix::{
25 Event, StateKey,
26 pdu::{PduCount, PduEvent},
27 },
28 ref_at,
29 utils::{
30 BoolExt, IterStream, ReadyExt, TryFutureExtExt,
31 math::usize_from_ruma,
32 result::FlatOk,
33 stream::{BroadbandExt, WidebandExt},
34 },
35};
36use tuwunel_service::Services;
37
38use self::{bump_stamp::room_bump_stamp, heroes::calculate_heroes};
39use super::{
40 super::{load_timeline_fallible, strip_prev_state},
41 Connection, ListIds, SyncInfo, WindowRoom,
42};
43use crate::client::{annotate_membership, ignored_filter, with_membership};
44
45#[derive(Debug)]
46pub(super) enum Failure {
47 Timeline(Error),
48 Payload(Error),
49}
50
51type ThreadCounts = BTreeMap<OwnedEventId, (u64, u64)>;
52
53#[tracing::instrument(
54 name = "room",
55 level = "debug",
56 skip_all,
57 fields(room_id, roomsince)
58)]
59pub(super) async fn handle_room(
60 sync_info: SyncInfo<'_>,
61 conn: &Connection,
62 window_room: &WindowRoom,
63 roomsince: u64,
64) -> Result<response::Room, Failure> {
65 let SyncInfo {
66 services,
67 sender_user,
68 previous_connection_pos,
69 ..
70 } = sync_info;
71 let WindowRoom { lists, membership, room_id, .. } = window_room;
72
73 debug_assert!(window_room.payload_is_fresh(roomsince), "Room payload should be fresh");
74
75 if matches!(*membership, Some(MembershipState::Leave | MembershipState::Ban)) {
76 return leave_or_ban_response(sync_info, conn, window_room, roomsince)
77 .map_err(Failure::Payload)
78 .await;
79 }
80
81 let is_invite = *membership == Some(MembershipState::Invite);
82
83 let encrypted = services.state_accessor.is_encrypted_room(room_id);
84
85 let (timeline_limit, required_state) = merged_room_details(conn, lists, room_id);
86
87 let timeline = is_invite.is_false().then_async(|| {
88 load_timeline_fallible(
89 services,
90 sender_user,
91 room_id,
92 PduCount::Normal(roomsince),
93 Some(PduCount::from(conn.next_batch)),
94 timeline_limit,
95 )
96 });
97
98 let timeline = timeline
99 .map(Option::transpose)
100 .map_err(Failure::Timeline);
101
102 let (encrypted, timeline) = join(encrypted, timeline).await;
103
104 let (timeline_pdus, limited, last_timeline_count) =
106 timeline?.unwrap_or_else(|| (Vec::new(), true, PduCount::default()));
107
108 let required_state = required_state
109 .into_iter()
110 .filter(|_| !timeline_pdus.is_empty())
111 .collect::<Vec<_>>();
112
113 let prev_batch = timeline_pdus
114 .first()
115 .map(at!(0))
116 .map(PduCount::into_unsigned)
117 .as_ref()
118 .map(ToString::to_string);
119
120 let bump_stamp = room_bump_stamp(
121 services,
122 sender_user,
123 room_id,
124 PduCount::Normal(roomsince),
125 PduCount::from(conn.next_batch),
126 last_timeline_count,
127 )
128 .map_err(Failure::Timeline)
129 .await?;
130
131 let required_state = collect_required_state(
132 services,
133 sender_user,
134 room_id,
135 &required_state,
136 &timeline_pdus,
137 encrypted,
138 );
139
140 let invite_state = is_invite.then_async(|| {
142 services
143 .state_cache
144 .invite_state(sender_user, room_id)
145 .ok()
146 });
147
148 let timeline = timeline_pdus
149 .iter()
150 .stream()
151 .filter_map(|item| ignored_filter(services, item.clone(), sender_user))
152 .wide_then(|(position, pdu)| {
153 with_membership(services, pdu, sender_user, encrypted).map(move |pdu| (position, pdu))
154 })
155 .wide_then(|(position, pdu)| {
156 services
157 .pdu_metadata
158 .bundle_aggregations(sender_user, pdu)
159 .map(move |pdu| (position, pdu))
160 })
161 .map(|(position, pdu)| (position, Event::into_format(pdu)))
162 .collect::<Vec<_>>();
163
164 let meta = room_meta_future(services, sender_user, room_id);
165 let events = join3(timeline, required_state, invite_state);
166 let member_counts = member_counts_future(services, room_id);
167 let notification_counts = notification_counts_future(services, sender_user, room_id);
168 let (
169 (room_name, room_avatar, is_dm),
170 (timeline, required_state, invite_state),
171 (joined_count, invited_count),
172 (highlight_count, notification_count, _last_notification_read, thread_counts),
173 ) = join4(meta, events, member_counts, notification_counts)
174 .boxed()
175 .await;
176
177 let (heroes, heroes_name, heroes_avatar) = resolve_heroes(
178 services,
179 sender_user,
180 room_id,
181 room_name.as_ref(),
182 room_avatar.as_deref(),
183 )
184 .await;
185
186 let previous_connection_pos = previous_connection_pos.filter(|_| !is_invite);
187 let (initial, num_live) =
188 room_timeline_metadata(roomsince, previous_connection_pos, &timeline);
189
190 let timeline = timeline.into_iter().map(at!(1)).collect();
191
192 Ok(response::Room {
193 initial,
194 lists: lists.clone(),
195 membership: membership.clone(),
196 name: room_name.or(heroes_name),
197 avatar: JsOption::from_option(room_avatar.or(heroes_avatar)),
198 is_dm,
199 heroes,
200 required_state,
201 invite_state: invite_state.flatten(),
202 prev_batch: prev_batch.as_deref().map(Into::into),
203 num_live,
204 limited,
205 timeline,
206 bump_stamp,
207 joined_count,
208 invited_count,
209 unread_notifications: merge_unread_notifications(
210 highlight_count,
211 notification_count,
212 &thread_counts,
213 ),
214 })
215}
216
217async fn leave_or_ban_response(
218 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
219 conn: &Connection,
220 WindowRoom { lists, membership, room_id, .. }: &WindowRoom,
221 roomsince: u64,
222) -> Result<response::Room> {
223 let member_event = services
226 .state_accessor
227 .room_state_get(room_id, &StateEventType::RoomMember, sender_user.as_str())
228 .map_ok(Event::into_format)
229 .await
230 .ok();
231
232 Ok(response::Room {
233 initial: roomsince.eq(&0).then_some(true),
234 lists: lists.clone(),
235 membership: membership.clone(),
236 prev_batch: Some(conn.next_batch.to_string().into()),
237 limited: true,
238 required_state: member_event.into_iter().collect(),
239 ..Default::default()
240 })
241}
242
243fn merged_room_details(
244 conn: &Connection,
245 lists: &ListIds,
246 room_id: &RoomId,
247) -> (usize, HashSet<(StateEventType, StateKey)>) {
248 lists
249 .iter()
250 .filter_map(|list_id| conn.lists.get(list_id))
251 .map(|list| &list.room_details)
252 .chain(conn.subscriptions.get(room_id))
253 .fold((0_usize, HashSet::new()), |(timeline_limit, mut required_state), config| {
254 required_state.extend(config.required_state.clone());
255 (timeline_limit.max(usize_from_ruma(config.timeline_limit)), required_state)
256 })
257}
258
259fn room_timeline_metadata<Event>(
260 roomsince: u64,
261 previous_connection_pos: Option<u64>,
262 timeline_pdus: &[(PduCount, Event)],
263) -> (Option<bool>, Option<UInt>) {
264 let initial = roomsince.eq(&0).then_some(true);
265 let num_live = previous_connection_pos
266 .map(PduCount::from)
267 .and_then(|previous_connection_pos| {
268 timeline_pdus
269 .iter()
270 .rev()
271 .map(|(position, _)| *position)
272 .take_while(|position| *position > previous_connection_pos)
273 .count()
274 .try_into()
275 .ok()
276 });
277
278 (initial, num_live)
279}
280
281async fn resolve_heroes(
282 services: &Services,
283 sender_user: &UserId,
284 room_id: &RoomId,
285 room_name: Option<&DisplayName>,
286 room_avatar: Option<&MxcUri>,
287) -> (Option<Heroes>, Option<DisplayName>, Option<OwnedMxcUri>) {
288 services
289 .config
290 .calculate_heroes
291 .then_async(|| calculate_heroes(services, sender_user, room_id, room_name, room_avatar))
292 .await
293 .unwrap_or_default()
294}
295
296fn room_meta_future<'a>(
297 services: &'a Services,
298 sender_user: &'a UserId,
299 room_id: &'a RoomId,
300) -> impl Future<Output = (Option<DisplayName>, Option<OwnedMxcUri>, Option<bool>)> + Send + 'a {
301 let room_name = services
302 .state_accessor
303 .get_name(room_id)
304 .map_ok(Into::into)
305 .map(Result::ok);
306
307 let room_avatar = services
308 .state_accessor
309 .get_avatar(room_id)
310 .map_ok(|content| content.url)
311 .ok()
312 .map(Option::flatten);
313
314 let is_dm = services
315 .state_accessor
316 .is_direct(room_id, sender_user)
317 .map(|is_dm| is_dm.then_some(is_dm));
318
319 join3(room_name, room_avatar, is_dm)
320}
321
322fn member_counts_future<'a>(
323 services: &'a Services,
324 room_id: &'a RoomId,
325) -> impl Future<Output = (Option<UInt>, Option<UInt>)> + Send + 'a {
326 let joined_count = services
327 .state_cache
328 .room_joined_count(room_id)
329 .map_ok(TryInto::try_into)
330 .map_ok(Result::ok)
331 .map(FlatOk::flat_ok);
332
333 let invited_count = services
334 .state_cache
335 .room_invited_count(room_id)
336 .map_ok(TryInto::try_into)
337 .map_ok(Result::ok)
338 .map(FlatOk::flat_ok);
339
340 join(joined_count, invited_count)
341}
342
343fn notification_counts_future<'a>(
344 services: &'a Services,
345 sender_user: &'a UserId,
346 room_id: &'a RoomId,
347) -> impl Future<Output = (Option<UInt>, Option<UInt>, Result<u64>, ThreadCounts)> + Send + 'a {
348 let highlight_count = services
349 .pusher
350 .highlight_count(sender_user, room_id)
351 .map(TryInto::try_into)
352 .map(Result::ok);
353
354 let notification_count = services
355 .pusher
356 .notification_count(sender_user, room_id)
357 .map(TryInto::try_into)
358 .map(Result::ok);
359
360 let last_read_count = services
361 .pusher
362 .last_notification_read(sender_user, room_id);
363
364 let thread_counts = services
365 .pusher
366 .thread_notification_counts(sender_user, room_id);
367
368 join4(highlight_count, notification_count, last_read_count, thread_counts)
369}
370
371fn merge_unread_notifications(
373 highlight_count: Option<UInt>,
374 notification_count: Option<UInt>,
375 thread_counts: &ThreadCounts,
376) -> UnreadNotificationsCount {
377 let (thread_notifications, thread_highlights) = thread_counts
378 .values()
379 .fold((0_u64, 0_u64), |(n, h), &(notifs, hl)| {
380 (n.saturating_add(notifs), h.saturating_add(hl))
381 });
382
383 let merge = |total: u64| {
384 move |count: UInt| count.saturating_add(UInt::try_from(total).unwrap_or_default())
385 };
386
387 UnreadNotificationsCount {
388 highlight_count: highlight_count.map(merge(thread_highlights)),
389 notification_count: notification_count.map(merge(thread_notifications)),
390 }
391}
392
393async fn collect_required_state(
394 services: &Services,
395 sender_user: &UserId,
396 room_id: &RoomId,
397 required_state: &[(StateEventType, StateKey)],
398 timeline_pdus: &[(PduCount, PduEvent)],
399 encrypted: bool,
400) -> Vec<Raw<AnySyncStateEvent>> {
401 let lazy = required_state
402 .iter()
403 .any(is_equal_to!(&(StateEventType::RoomMember, "$LAZY".into())));
404
405 let timeline_senders = timeline_pdus
406 .iter()
407 .filter(|_| lazy)
408 .map(ref_at!(1))
409 .map(Event::sender)
410 .map(UserId::as_str);
411
412 let timeline_member_targets = timeline_pdus
413 .iter()
414 .filter(|_| lazy)
415 .map(ref_at!(1))
416 .filter(|event| *event.event_type() == TimelineEventType::RoomMember)
417 .filter_map(Event::state_key);
418
419 let timeline_senders = timeline_senders
420 .chain(timeline_member_targets)
421 .sorted_unstable()
422 .dedup()
423 .map(|sender| (StateEventType::RoomMember, StateKey::from_str(sender)))
424 .collect::<Vec<_>>();
425
426 let wildcard_types: Vec<StateEventType> = required_state
427 .iter()
428 .filter(|(_, state_key)| state_key == "*")
429 .map(|(event_type, _)| event_type.clone())
430 .collect();
431
432 let wildcard_state: Vec<(StateEventType, StateKey)> = wildcard_types
433 .into_iter()
434 .stream()
435 .broad_then(|event_type| wildcard_state_keys(services, room_id, event_type))
436 .concat()
437 .await;
438
439 let in_timeline = |event: &PduEvent| {
440 timeline_pdus
441 .iter()
442 .map(ref_at!(1))
443 .map(Event::event_id)
444 .any(is_equal_to!(event.event_id()))
445 };
446
447 required_state
448 .iter()
449 .cloned()
450 .stream()
451 .chain(wildcard_state.into_iter().stream())
452 .chain(timeline_senders.into_iter().stream())
453 .broad_filter_map(async |state| {
454 let state_key: StateKey = match state.1.as_str() {
455 | "$LAZY" | "*" => return None,
456 | "$ME" => sender_user.as_str().into(),
457 | _ => state.1.clone(),
458 };
459
460 let mut pdu = services
461 .state_accessor
462 .room_state_get(room_id, &state.0, &state_key)
463 .map_ok(Event::into_pdu)
464 .ok()
465 .await?;
466
467 annotate_membership(services, &mut pdu, sender_user, encrypted).await;
468
469 let pdu = strip_prev_state(pdu, sender_user, in_timeline);
470
471 Some(Event::into_format(pdu))
472 })
473 .collect()
474 .await
475}
476
477async fn wildcard_state_keys(
478 services: &Services,
479 room_id: &RoomId,
480 event_type: StateEventType,
481) -> Vec<(StateEventType, StateKey)> {
482 services
483 .state_accessor
484 .room_state_keys(room_id, &event_type)
485 .ready_filter_map(Result::ok)
486 .map(|state_key| (event_type.clone(), state_key))
487 .collect()
488 .await
489}
490
491#[cfg(test)]
492mod tests {
493 use ruma::{UInt, uint};
494 use tuwunel_core::matrix::pdu::PduCount;
495
496 use super::room_timeline_metadata;
497
498 fn timeline(positions: &[u64]) -> Vec<(PduCount, ())> {
499 positions
500 .iter()
501 .copied()
502 .map(|position| (PduCount::Normal(position), ()))
503 .collect()
504 }
505
506 #[test]
507 fn first_connection_timeline_is_initial_and_historical() {
508 let (initial, num_live) = room_timeline_metadata(0, None, &timeline(&[8, 9, 10]));
509
510 assert_eq!(initial, Some(true));
511 assert_eq!(num_live, None);
512 }
513
514 #[test]
515 fn incremental_new_room_has_one_live_event() {
516 let (initial, num_live) = room_timeline_metadata(0, Some(10), &timeline(&[8, 9, 11]));
517
518 assert_eq!(initial, Some(true));
519 assert_eq!(num_live, Some(uint!(1)));
520 }
521
522 #[test]
523 fn incremental_range_expansion_has_no_live_events() {
524 let (initial, num_live) = room_timeline_metadata(0, Some(10), &timeline(&[7, 8, 9]));
525
526 assert_eq!(initial, Some(true));
527 assert_eq!(num_live, Some(uint!(0)));
528 }
529
530 #[test]
531 fn incremental_timeline_counts_only_live_suffix() {
532 let (initial, num_live) = room_timeline_metadata(5, Some(10), &timeline(&[8, 9, 11, 12]));
533
534 assert_eq!(initial, None);
535 assert_eq!(num_live, Some(uint!(2)));
536 }
537
538 #[test]
539 fn limited_timeline_counts_only_returned_live_events() {
540 let returned_timeline = timeline(&[14, 15]);
542 let (_, num_live) = room_timeline_metadata(5, Some(10), &returned_timeline);
543
544 assert_eq!(num_live, Some(uint!(2)));
545 let timeline_len =
546 UInt::try_from(returned_timeline.len()).expect("timeline length fits UInt");
547
548 assert!(num_live.expect("incremental response") <= timeline_len);
549 }
550}