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