Skip to main content

tuwunel_api/client/sync/v5/
rooms.rs

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	// A failed load must fail the room, else roomsince advances past unsent events.
105	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	// TODO: figure out a timestamp we can use for remote invites
141	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	// A rejected federated invite has no resolved state; the retraction still
224	// delivers on the membership alone.
225	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
371// MSC3771/MSC3773: SSS v5 has no per-thread bucket; fold into the room total.
372fn 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		// Earlier live events at positions 11 through 13 were truncated.
541		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}