Skip to main content

tuwunel_api_client/sync/v3/joined/
mod.rs

1use std::collections::{BTreeMap, HashSet};
2
3use futures::{
4	FutureExt, StreamExt, TryFutureExt,
5	future::{join, join3, join4},
6};
7use ruma::{
8	DeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UInt, UserId,
9	api::client::{
10		filter::FilterDefinition,
11		sync::sync_events::{
12			UnreadNotificationsCount,
13			v3::{Ephemeral, JoinedRoom, RoomAccountData, RoomSummary, StateEvents, Timeline},
14		},
15	},
16	events::{
17		AnyRawAccountDataEvent, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent,
18		AnySyncStateEvent, StateEventType,
19		TimelineEventType::*,
20		room::member::{MembershipState, RoomMemberEventContent},
21	},
22	serde::Raw,
23};
24use tuwunel_core::{
25	Result, at, extract_variant, is_equal_to, pair_of, ref_at,
26	utils::{
27		BoolExt, IterStream, ReadyExt, TryFutureExtExt, math::ruma_from_u64, result::MapExpect,
28		stream::WidebandExt,
29	},
30	warn,
31};
32use tuwunel_matrix::{
33	Event,
34	event::{Matches, trim_event_fields},
35	pdu::{PduCount, PduEvent},
36};
37use tuwunel_service::{
38	Services,
39	rooms::{lazy_loading::Witness, read_receipt::PrivateReadEvents, short::ShortStateHash},
40};
41
42use self::notifications::{
43	GateInputs, Gates as NotificationGates, assemble_threads as assemble_thread_notifications,
44	assemble_unread as assemble_unread_notifications,
45	compute_gates as compute_notification_gates, count_futures as notification_count_futures,
46	in_window, send_thread_counts,
47};
48use super::{
49	super::{load_timeline, strip_state_unsigned, timeline_prev_batch},
50	StateAfter, StateChangeParams, StateChanges,
51	state::{
52		UserMetadata, calculate_state_changes, gather_room_metadata, gather_user_metadata,
53		typings_event_for_user,
54	},
55};
56use crate::{ignored_filter, is_empty_account_data_event, with_membership};
57
58pub(super) mod notifications;
59
60pub(super) struct RoomMetadata {
61	pub(super) since_shortstatehash: Option<ShortStateHash>,
62	pub(super) horizon_shortstatehash: Option<ShortStateHash>,
63	pub(super) after_shortstatehash: Option<ShortStateHash>,
64	pub(super) current_shortstatehash: Option<ShortStateHash>,
65	pub(super) receipt_events: Vec<(OwnedUserId, Raw<AnySyncEphemeralRoomEvent>)>,
66	pub(super) encrypted_room: Option<bool>,
67}
68
69struct BuildJoinedRoom {
70	receipt_events: Vec<(OwnedUserId, Raw<AnySyncEphemeralRoomEvent>)>,
71	typing_events: Vec<Raw<AnySyncEphemeralRoomEvent>>,
72	private_read_events: Option<PrivateReadEvents>,
73	state_events: Vec<Raw<AnySyncStateEvent>>,
74	account_data_events: Vec<Raw<AnyRoomAccountDataEvent>>,
75	room_events: Vec<PduEvent>,
76	heroes: Option<Vec<OwnedUserId>>,
77	joined_member_count: Option<u64>,
78	invited_member_count: Option<u64>,
79	unread_notifications: UnreadNotificationsCount,
80	state_after: StateAfter,
81	limited: bool,
82	joined_since_last_sync: bool,
83	prev_batch: Option<PduCount>,
84}
85
86struct FinalizeJoinedRoom<'a, S, F, W> {
87	services: &'a Services,
88	sender_user: &'a UserId,
89	room_id: &'a RoomId,
90	filter: &'a FilterDefinition,
91	state_events: S,
92	aggregates: JoinAggregates,
93	receipt_events: Vec<(OwnedUserId, Raw<AnySyncEphemeralRoomEvent>)>,
94	heroes: Option<Vec<OwnedUserId>>,
95	joined_member_count: Option<u64>,
96	invited_member_count: Option<u64>,
97	thread_last_reads: Option<&'a BTreeMap<OwnedEventId, u64>>,
98	send_notification_count_filter: F,
99	flags: FinalizeJoinFlags,
100	in_window: W,
101	prev_batch: Option<PduCount>,
102}
103
104pub(super) type ThreadCounts = BTreeMap<OwnedEventId, (u64, u64)>;
105
106struct JoinAggregates {
107	room_events: Vec<PduEvent>,
108	account_data_events: Vec<Raw<AnyRoomAccountDataEvent>>,
109	typing_events: Vec<Raw<AnySyncEphemeralRoomEvent>>,
110	private_read_events: Option<PrivateReadEvents>,
111	notification_count: Option<UInt>,
112	highlight_count: Option<UInt>,
113	thread_counts: Option<ThreadCounts>,
114	device_list_updates: HashSet<OwnedUserId>,
115	left_encrypted_users: HashSet<OwnedUserId>,
116}
117
118struct FinalizeJoinFlags {
119	encrypted: bool,
120	full_state: bool,
121	state_after: StateAfter,
122	limited: bool,
123	joined_since_last_sync: bool,
124	initial: bool,
125}
126
127#[tracing::instrument(
128	target = "tuwunel_api_client::sync::v3",
129	name = "joined",
130	level = "debug",
131	skip_all,
132	fields(
133		room_id = ?room_id,
134	),
135)]
136#[expect(clippy::too_many_arguments)]
137pub(super) async fn load_room(
138	services: &Services,
139	sender_user: &UserId,
140	sender_device: Option<&DeviceId>,
141	ref room_id: OwnedRoomId,
142	since: u64,
143	next_batch: u64,
144	full_state: bool,
145	state_after: StateAfter,
146	filter: &FilterDefinition,
147) -> Result<(JoinedRoom, HashSet<OwnedUserId>, HashSet<OwnedUserId>)> {
148	let initial = since == 0;
149	let (timeline_pdus, limited, last_timeline_count) =
150		load_join_timeline(services, sender_user, room_id, since, next_batch, filter).await?;
151
152	let timeline_changed = last_timeline_count.into_unsigned() > since;
153	debug_assert!(
154		timeline_pdus.is_empty() || timeline_changed,
155		"if timeline events, last_timeline_count must be in the since window."
156	);
157
158	let RoomMetadata {
159		since_shortstatehash,
160		horizon_shortstatehash,
161		after_shortstatehash,
162		current_shortstatehash,
163		receipt_events,
164		encrypted_room,
165	} = gather_room_metadata(
166		services,
167		sender_user,
168		room_id,
169		since,
170		next_batch,
171		&timeline_pdus,
172		last_timeline_count,
173		timeline_changed || full_state || initial,
174		state_after,
175	)
176	.boxed() // size firewall
177	.await?;
178
179	let UserMetadata {
180		witness,
181		last_notification_read,
182		thread_last_reads,
183		last_privateread_update,
184		joined_since_last_sync,
185	} = gather_user_metadata(
186		services,
187		sender_user,
188		sender_device,
189		room_id,
190		filter,
191		&timeline_pdus,
192		&receipt_events,
193		since,
194		initial,
195		timeline_changed,
196		encrypted_room,
197	)
198	.boxed() // size firewall
199	.await;
200
201	let (
202		state_after,
203		StateChanges {
204			heroes,
205			joined_member_count,
206			invited_member_count,
207			mut state_events,
208			lazy_members,
209		},
210	) = compute_join_state_changes(
211		services,
212		sender_user,
213		room_id,
214		full_state || initial,
215		state_after,
216		since_shortstatehash,
217		horizon_shortstatehash,
218		after_shortstatehash,
219		current_shortstatehash,
220		joined_since_last_sync,
221		witness.as_ref(),
222	)
223	.await?;
224
225	let quiet_round = timeline_pdus.is_empty();
226
227	let joined_sender_member = take_sender_membership_for_join(
228		&mut state_events,
229		sender_user,
230		joined_since_last_sync,
231		quiet_round,
232		initial,
233	);
234
235	let prev_batch =
236		compute_join_prev_batch(&timeline_pdus, joined_sender_member.as_ref(), since, next_batch);
237
238	let in_window = in_window(since, next_batch);
239
240	let NotificationGates {
241		send_notification_counts,
242		send_notification_count_filter,
243	} = compute_notification_gates(
244		GateInputs {
245			last_notification_read,
246			thread_last_reads: &thread_last_reads,
247			quiet_round,
248			since,
249		},
250		in_window,
251	);
252
253	// `encrypted_room` is `Some` whenever timeline or state events are emitted.
254	let encrypted = encrypted_room.unwrap_or(false);
255
256	let aggregates = await_join_aggregates(
257		services,
258		sender_user,
259		room_id,
260		&state_events,
261		timeline_pdus,
262		joined_sender_member,
263		encrypted,
264		initial,
265		since,
266		next_batch,
267		last_privateread_update,
268		send_notification_counts,
269		filter,
270	)
271	.await;
272
273	let state_events = state_events.into_iter().chain(lazy_members);
274
275	let flags = FinalizeJoinFlags {
276		encrypted,
277		full_state,
278		state_after,
279		limited,
280		joined_since_last_sync,
281		initial,
282	};
283
284	let finalize = FinalizeJoinedRoom {
285		services,
286		sender_user,
287		room_id,
288		filter,
289		state_events,
290		aggregates,
291		receipt_events,
292		heroes,
293		joined_member_count,
294		invited_member_count,
295		thread_last_reads: quiet_round.then_some(&thread_last_reads),
296		send_notification_count_filter,
297		flags,
298		in_window,
299		prev_batch,
300	};
301
302	let (joined_room, device_list_updates, left_encrypted_users) =
303		finalize_joined_room(finalize).await;
304
305	Ok((joined_room, device_list_updates, left_encrypted_users))
306}
307
308#[expect(clippy::too_many_arguments)]
309async fn compute_join_state_changes(
310	services: &Services,
311	sender_user: &UserId,
312	room_id: &RoomId,
313	full_state: bool,
314	state_after: StateAfter,
315	since_shortstatehash: Option<ShortStateHash>,
316	horizon_shortstatehash: Option<ShortStateHash>,
317	after_shortstatehash: Option<ShortStateHash>,
318	current_shortstatehash: Option<ShortStateHash>,
319	joined_since_last_sync: bool,
320	witness: Option<&Witness>,
321) -> Result<(StateAfter, StateChanges)> {
322	let Some(current_shortstatehash) = current_shortstatehash else {
323		return Ok((state_after, StateChanges::default()));
324	};
325
326	let state_changes =
327		calculate_state_changes(services, sender_user, room_id, StateChangeParams {
328			full_state,
329			state_after,
330			since_shortstatehash,
331			horizon_shortstatehash,
332			after_shortstatehash,
333			current_shortstatehash,
334			joined_since_last_sync,
335			witness,
336			include_heroes: true,
337		})
338		.await;
339
340	let incremental = !full_state && !joined_since_last_sync && since_shortstatehash.is_some();
341
342	if !state_after.requested() || incremental {
343		return state_changes.map(|state_changes| (state_after, state_changes));
344	}
345
346	match state_changes {
347		| Ok(state_changes) => Ok((state_after, state_changes)),
348		| Err(after_error) => {
349			let after_boundary = after_shortstatehash.unwrap_or(current_shortstatehash);
350			let legacy_boundary = horizon_shortstatehash.unwrap_or(current_shortstatehash);
351
352			warn!(target: "tuwunel_api_client::sync::v3",
353				%room_id,
354				%after_boundary,
355				?after_error,
356				"Failed to load requested state-after boundary; retrying legacy state."
357			);
358
359			calculate_state_changes(services, sender_user, room_id, StateChangeParams {
360				full_state,
361				state_after: StateAfter::Off,
362				since_shortstatehash,
363				horizon_shortstatehash,
364				after_shortstatehash,
365				current_shortstatehash,
366				joined_since_last_sync,
367				witness,
368				include_heroes: false,
369			})
370			.await
371			.inspect_err(|legacy_error| {
372				warn!(target: "tuwunel_api_client::sync::v3",
373					%room_id,
374					%after_boundary,
375					%legacy_boundary,
376					?after_error,
377					?legacy_error,
378					"Failed to load state-after and legacy state boundaries."
379				);
380			})
381			.map(|state_changes| (StateAfter::Off, state_changes))
382		},
383	}
384}
385
386fn compute_join_prev_batch(
387	timeline_pdus: &[(PduCount, PduEvent)],
388	joined_sender_member: Option<&PduEvent>,
389	since: u64,
390	next_batch: u64,
391) -> Option<PduCount> {
392	timeline_prev_batch(timeline_pdus, PduCount::Normal(next_batch)).or_else(|| {
393		joined_sender_member
394			.is_some()
395			.then_some(since)
396			.map(Into::into)
397	})
398}
399
400#[expect(clippy::too_many_arguments)]
401async fn assemble_join_state_events(
402	services: &Services,
403	state_events: impl Iterator<Item = PduEvent> + Send,
404	sender_user: &UserId,
405	encrypted: bool,
406	room_events: &[PduEvent],
407	filter: &FilterDefinition,
408	full_state: bool,
409	state_after: StateAfter,
410) -> Vec<Raw<AnySyncStateEvent>> {
411	let is_in_timeline = |event: &PduEvent| {
412		room_events
413			.iter()
414			.map(Event::event_id)
415			.any(is_equal_to!(event.event_id()))
416	};
417
418	// MSC4222: when the client opts into `state_after`, state events that
419	// took effect within the timeline appear in both the timeline and the
420	// state section, so the in-timeline exclusion is bypassed.
421	let include_in_state = |event: &PduEvent| {
422		let filter = &filter.room.state;
423		filter.matches(event) && (full_state || state_after.requested() || !is_in_timeline(event))
424	};
425
426	assemble_state_events(
427		services,
428		state_events,
429		sender_user,
430		encrypted,
431		include_in_state,
432		&is_in_timeline,
433		filter.event_fields.as_deref(),
434	)
435	.await
436}
437
438async fn load_join_timeline(
439	services: &Services,
440	sender_user: &UserId,
441	room_id: &RoomId,
442	since: u64,
443	next_batch: u64,
444	filter: &FilterDefinition,
445) -> Result<(Vec<(PduCount, PduEvent)>, bool, PduCount)> {
446	let timeline_limit: usize = filter
447		.room
448		.timeline
449		.limit
450		.map(TryInto::try_into)
451		.map_expect("UInt to usize")
452		.unwrap_or(10)
453		.min(100);
454
455	load_timeline(
456		services,
457		sender_user,
458		room_id,
459		PduCount::Normal(since),
460		Some(PduCount::Normal(next_batch)),
461		timeline_limit,
462	)
463	.await
464}
465
466#[expect(clippy::too_many_arguments)]
467async fn await_join_aggregates(
468	services: &Services,
469	sender_user: &UserId,
470	room_id: &RoomId,
471	state_events: &[PduEvent],
472	timeline_pdus: Vec<(PduCount, PduEvent)>,
473	joined_sender_member: Option<PduEvent>,
474	encrypted: bool,
475	initial: bool,
476	since: u64,
477	next_batch: u64,
478	last_privateread_update: u64,
479	send_notification_counts: bool,
480	filter: &FilterDefinition,
481) -> JoinAggregates {
482	let (notification_count, highlight_count, thread_counts) =
483		notification_count_futures(services, sender_user, room_id, send_notification_counts);
484
485	let private_read_events = last_privateread_update.gt(&since).then_async(|| {
486		services
487			.read_receipt
488			.private_read_get(room_id, sender_user)
489			.unwrap_or_default()
490	});
491
492	let typing_events = gather_typing_events(services, room_id, sender_user, since);
493
494	let device_list_updates = gather_device_list_updates(
495		services,
496		room_id,
497		timeline_membership_changes(&timeline_pdus, initial),
498		state_events,
499		initial,
500		since,
501		next_batch,
502	);
503
504	let room_events = collect_room_events(
505		services,
506		sender_user,
507		timeline_pdus,
508		joined_sender_member,
509		encrypted,
510		filter,
511	);
512
513	let account_data_events = collect_room_account_data(services, sender_user, room_id, since);
514
515	let (
516		(room_events, account_data_events),
517		(typing_events, private_read_events),
518		(notification_count, highlight_count, thread_counts),
519		(device_list_updates, left_encrypted_users),
520	) = join4(
521		join(room_events, account_data_events),
522		join(typing_events, private_read_events),
523		join3(notification_count, highlight_count, thread_counts),
524		device_list_updates,
525	)
526	.boxed() // size firewall
527	.await;
528
529	JoinAggregates {
530		room_events,
531		account_data_events,
532		typing_events,
533		private_read_events,
534		notification_count,
535		highlight_count,
536		thread_counts,
537		device_list_updates,
538		left_encrypted_users,
539	}
540}
541
542async fn finalize_joined_room<S, F, W>(
543	FinalizeJoinedRoom {
544		services,
545		sender_user,
546		room_id,
547		filter,
548		state_events,
549		aggregates,
550		receipt_events,
551		heroes,
552		joined_member_count,
553		invited_member_count,
554		thread_last_reads,
555		send_notification_count_filter,
556		flags,
557		in_window,
558		prev_batch,
559	}: FinalizeJoinedRoom<'_, S, F, W>,
560) -> (JoinedRoom, HashSet<OwnedUserId>, HashSet<OwnedUserId>)
561where
562	S: Iterator<Item = PduEvent> + Send,
563	F: Fn(&UInt) -> bool,
564	W: Fn(u64) -> bool,
565{
566	let JoinAggregates {
567		room_events,
568		account_data_events,
569		typing_events,
570		private_read_events,
571		notification_count,
572		highlight_count,
573		thread_counts,
574		device_list_updates,
575		left_encrypted_users,
576	} = aggregates;
577
578	let FinalizeJoinFlags {
579		encrypted,
580		full_state,
581		state_after,
582		limited,
583		joined_since_last_sync,
584		initial,
585	} = flags;
586
587	let want_thread_unread = filter.room.timeline.unread_thread_notifications;
588
589	let state_events = assemble_join_state_events(
590		services,
591		state_events,
592		sender_user,
593		encrypted,
594		&room_events,
595		filter,
596		full_state,
597		state_after,
598	)
599	.await;
600
601	let unread_notifications = assemble_unread_notifications(
602		notification_count,
603		highlight_count,
604		thread_counts.as_ref(),
605		send_notification_count_filter,
606		want_thread_unread,
607	);
608
609	let args = BuildJoinedRoom {
610		receipt_events,
611		typing_events,
612		private_read_events,
613		state_events,
614		account_data_events,
615		room_events,
616		heroes,
617		joined_member_count,
618		invited_member_count,
619		unread_notifications,
620		state_after,
621		limited,
622		joined_since_last_sync,
623		prev_batch,
624	};
625
626	let joined_room = build_joined_room(args, filter.event_fields.as_deref());
627
628	// Initial and busy rounds (no last-reads) send every thread's zero.
629	let send_thread_zero = move |root: &OwnedEventId| {
630		initial
631			|| thread_last_reads
632				.is_none_or(|reads| reads.get(root).copied().is_some_and(&in_window))
633	};
634
635	let send_threads = want_thread_unread
636		&& send_thread_counts(
637			!joined_room.is_empty(),
638			thread_counts.iter().flat_map(ThreadCounts::keys),
639			&send_thread_zero,
640		);
641
642	let thread_counts = send_threads
643		.then_async(async move || match thread_counts {
644			| Some(thread_counts) => thread_counts,
645			| None =>
646				services
647					.pusher
648					.thread_notification_counts(sender_user, room_id)
649					.await,
650		})
651		.await;
652
653	let unread_thread_notifications =
654		assemble_thread_notifications(thread_counts, send_thread_zero);
655
656	let joined_room = JoinedRoom {
657		unread_thread_notifications,
658		..joined_room
659	};
660
661	(joined_room, device_list_updates, left_encrypted_users)
662}
663
664fn build_joined_room(args: BuildJoinedRoom, event_fields: Option<&[String]>) -> JoinedRoom {
665	let BuildJoinedRoom {
666		receipt_events,
667		typing_events,
668		private_read_events,
669		state_events,
670		account_data_events,
671		room_events,
672		heroes,
673		joined_member_count,
674		invited_member_count,
675		unread_notifications,
676		state_after,
677		limited,
678		joined_since_last_sync,
679		prev_batch,
680	} = args;
681
682	let edus: Vec<Raw<AnySyncEphemeralRoomEvent>> = receipt_events
683		.into_iter()
684		.map(at!(1))
685		.chain(typing_events)
686		.chain(private_read_events.into_iter().flatten())
687		.collect();
688
689	let state = state_after.wrap(StateEvents { events: state_events });
690
691	let heroes = heroes
692		.into_iter()
693		.flatten()
694		.map(TryInto::try_into)
695		.filter_map(Result::ok)
696		.collect();
697
698	JoinedRoom {
699		account_data: RoomAccountData { events: account_data_events },
700		ephemeral: Ephemeral { events: edus },
701		state,
702		summary: RoomSummary {
703			joined_member_count: joined_member_count.map(ruma_from_u64),
704			invited_member_count: invited_member_count.map(ruma_from_u64),
705			heroes,
706		},
707		timeline: Timeline {
708			limited: limited || joined_since_last_sync,
709			prev_batch: prev_batch.as_ref().map(ToString::to_string),
710			events: room_events
711				.into_iter()
712				.map(|pdu| trim_event_fields(pdu.into_format(), event_fields))
713				.collect(),
714		},
715		unread_notifications,
716		// The caller adds these once it knows the room is reported.
717		unread_thread_notifications: BTreeMap::new(),
718	}
719}
720
721fn collect_room_events<'a>(
722	services: &'a Services,
723	sender_user: &'a UserId,
724	timeline_pdus: Vec<(PduCount, PduEvent)>,
725	joined_sender_member: Option<PduEvent>,
726	encrypted: bool,
727	filter: &'a FilterDefinition,
728) -> impl Future<Output = Vec<PduEvent>> + Send + 'a {
729	let include_in_timeline = |event: &PduEvent| filter.room.timeline.matches(event);
730	timeline_pdus
731		.into_iter()
732		.stream()
733		.wide_filter_map(|item| ignored_filter(services, item, sender_user))
734		.map(at!(1))
735		.chain(joined_sender_member.into_iter().stream())
736		.ready_filter(include_in_timeline)
737		.wide_then(move |pdu| with_membership(services, pdu, sender_user, encrypted))
738		.wide_then(move |pdu| {
739			services
740				.pdu_metadata
741				.bundle_aggregations(sender_user, pdu)
742		})
743		.collect::<Vec<_>>()
744}
745
746fn collect_room_account_data<'a>(
747	services: &'a Services,
748	sender_user: &'a UserId,
749	room_id: &'a RoomId,
750	since: u64,
751) -> impl Future<Output = Vec<Raw<AnyRoomAccountDataEvent>>> + Send + 'a {
752	services
753		.account_data
754		.changes_since(Some(room_id), sender_user, since, None)
755		.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
756		.ready_filter(move |e| since != 0 || !is_empty_account_data_event(e))
757		.collect()
758}
759
760fn take_sender_membership_for_join(
761	state_events: &mut Vec<PduEvent>,
762	sender_user: &UserId,
763	joined_since_last_sync: bool,
764	timeline_empty: bool,
765	initial: bool,
766) -> Option<PduEvent> {
767	if !(joined_since_last_sync && timeline_empty && !initial) {
768		return None;
769	}
770
771	let is_sender_membership = |event: &PduEvent| {
772		*event.event_type() == StateEventType::RoomMember.into()
773			&& event
774				.state_key()
775				.is_some_and(is_equal_to!(sender_user.as_str()))
776	};
777
778	state_events
779		.iter()
780		.position(is_sender_membership)
781		.map(|pos| state_events.swap_remove(pos))
782}
783
784async fn gather_typing_events(
785	services: &Services,
786	room_id: &RoomId,
787	sender_user: &UserId,
788	since: u64,
789) -> Vec<Raw<AnySyncEphemeralRoomEvent>> {
790	services
791		.typing
792		.last_typing_update(room_id)
793		.and_then(async |count| {
794			if count <= since {
795				return Ok(Vec::<Raw<AnySyncEphemeralRoomEvent>>::new());
796			}
797
798			let typings = typings_event_for_user(services, room_id, sender_user).await?;
799
800			Ok(vec![serde_json::from_str(&serde_json::to_string(&typings)?)?])
801		})
802		.unwrap_or(Vec::new())
803		.await
804}
805
806fn timeline_membership_changes(
807	timeline_pdus: &[(PduCount, PduEvent)],
808	initial: bool,
809) -> Vec<(MembershipState, OwnedUserId)> {
810	timeline_pdus
811		.iter()
812		.filter(|_| !initial)
813		.map(ref_at!(1))
814		.filter_map(extract_membership)
815		.collect::<Vec<_>>()
816}
817
818fn extract_membership(event: &PduEvent) -> Option<(MembershipState, OwnedUserId)> {
819	let content: RoomMemberEventContent = event.get_content().ok()?;
820	let user_id: OwnedUserId = event.state_key()?.parse().ok()?;
821
822	Some((content.membership, user_id))
823}
824
825async fn gather_device_list_updates(
826	services: &Services,
827	room_id: &RoomId,
828	timeline_membership_changes: Vec<(MembershipState, OwnedUserId)>,
829	state_events: &[PduEvent],
830	initial: bool,
831	since: u64,
832	next_batch: u64,
833) -> (HashSet<OwnedUserId>, HashSet<OwnedUserId>) {
834	let keys_changed = services
835		.users
836		.room_keys_changed(room_id, since, Some(next_batch))
837		.map(|(user_id, _)| user_id)
838		.map(ToOwned::to_owned)
839		.collect()
840		.await;
841
842	let lists = (keys_changed, HashSet::new());
843
844	state_events
845		.iter()
846		.filter(|_| !initial)
847		.filter(|state_event| *state_event.event_type() == RoomMember)
848		.filter_map(extract_membership)
849		.chain(timeline_membership_changes)
850		.fold(lists, |(mut dlu, mut leu): pair_of!(HashSet<_>), (membership, user_id)| {
851			use MembershipState::*;
852
853			match membership {
854				| Join => dlu.insert(user_id),
855				| Leave => leu.insert(user_id),
856				| _ => false,
857			};
858
859			(dlu, leu)
860		})
861}
862
863async fn assemble_state_events(
864	services: &Services,
865	state_events: impl Iterator<Item = PduEvent> + Send,
866	sender_user: &UserId,
867	encrypted: bool,
868	include_in_state: impl Fn(&PduEvent) -> bool + Send + Sync,
869	in_timeline: impl Fn(&PduEvent) -> bool + Send + Sync,
870	event_fields: Option<&[String]>,
871) -> Vec<Raw<AnySyncStateEvent>> {
872	state_events
873		.filter(include_in_state)
874		.stream()
875		.wide_then(|pdu| with_membership(services, pdu, sender_user, encrypted))
876		.map(|pdu| strip_state_unsigned(pdu, sender_user, &in_timeline))
877		.map(|pdu| trim_event_fields(pdu.into_format(), event_fields))
878		.collect()
879		.await
880}