use std::collections::HashSet; use futures::{ FutureExt, StreamExt, TryFutureExt, future::{OptionFuture, join, join3}, stream::once, }; use ruma::{ OwnedUserId, RoomId, api::client::sync::sync_events::{DeviceLists, v5::response}, events::{ StateEventType, TimelineEventType, room::member::{MembershipState, RoomMemberEventContent}, }, }; use tuwunel_core::{ Result, error, matrix::{Event, pdu::PduCount}, pair_of, utils::{ BoolExt, IterStream, ReadyExt, TryFutureExtExt, future::OptionStream, stream::BroadbandExt, }, }; use tuwunel_service::Services; use super::{SyncInfo, share_encrypted_room}; #[tracing::instrument(level = "trace", skip_all, fields(globalsince, next_batch,))] pub(super) async fn collect( services: &Services, syncinfo: SyncInfo<'_>, next_batch: u64, ) -> Result { let &(sender_user, sender_device, globalsince, _) = &syncinfo; let keys_changed = services .users .keys_changed(sender_user, globalsince, Some(next_batch)) .map(ToOwned::to_owned) .collect::>() .map(|changed| (changed, HashSet::new())); let (changed, left) = (HashSet::new(), HashSet::new()); let (changed, left) = services .state_cache .rooms_joined(sender_user) .map(ToOwned::to_owned) .broad_filter_map(async |room_id| { collect_room(services, syncinfo, next_batch, &room_id) .await .ok() }) .chain(once(keys_changed)) .ready_fold((changed, left), |(mut changed, mut left), room| { changed.extend(room.0); left.extend(room.1); (changed, left) }) .await; let left = left .into_iter() .stream() .filter_map(async |user_id| { share_encrypted_room(services, sender_user, &user_id, None) .await .is_false() .then_some(user_id) }) .collect(); let device_one_time_keys_count = services .users .last_one_time_keys_update(sender_user) .then(|since| -> OptionFuture<_> { since .gt(&globalsince) .then(|| { services .users .count_one_time_keys(sender_user, sender_device) }) .into() }) .map(Option::unwrap_or_default); let (left, device_one_time_keys_count) = join(left, device_one_time_keys_count) .boxed() .await; Ok(response::E2EE { device_one_time_keys_count, device_unused_fallback_key_types: None, device_lists: DeviceLists { changed: changed.into_iter().collect(), left, }, }) } #[tracing::instrument(level = "trace", skip_all, fields(room_id))] async fn collect_room( services: &Services, (sender_user, _, globalsince, _): SyncInfo<'_>, next_batch: u64, room_id: &RoomId, ) -> Result)> { let current_shortstatehash = services .state .get_room_shortstatehash(room_id) .inspect_err(|e| error!("Room {room_id} has no state: {e}")); let room_keys_changed = services .users .room_keys_changed(room_id, globalsince, Some(next_batch)) .map(|(user_id, _)| user_id) .map(ToOwned::to_owned) .collect::>(); let (current_shortstatehash, device_list_changed) = join(current_shortstatehash, room_keys_changed) .boxed() .await; let lists = (device_list_changed, HashSet::new()); let Ok(current_shortstatehash) = current_shortstatehash else { return Ok(lists); }; if current_shortstatehash <= globalsince { return Ok(lists); } let Ok(since_shortstatehash) = services .timeline .prev_shortstatehash(room_id, PduCount::Normal(globalsince).saturating_add(1)) .await else { return Ok(lists); }; if since_shortstatehash == current_shortstatehash { return Ok(lists); } let encrypted_room = services .state_accessor .state_get(current_shortstatehash, &StateEventType::RoomEncryption, "") .is_ok(); let since_encryption = services .state_accessor .state_get(since_shortstatehash, &StateEventType::RoomEncryption, "") .is_ok(); let sender_joined_count = services .state_cache .get_joined_count(room_id, sender_user); let (encrypted_room, since_encryption, sender_joined_count) = join3(encrypted_room, since_encryption, sender_joined_count).await; if !encrypted_room { return Ok(lists); } let encrypted_since_last_sync = !since_encryption; let joined_since_last_sync = sender_joined_count.is_ok_and(|count| count > globalsince); let joined_members_burst: OptionFuture<_> = (joined_since_last_sync || encrypted_since_last_sync) .then(|| { services .state_cache .room_members(room_id) .ready_filter(|&user_id| user_id != sender_user) .map(ToOwned::to_owned) .map(|user_id| (MembershipState::Join, user_id)) .into_future() }) .into(); services .state_accessor .state_added((since_shortstatehash, current_shortstatehash)) .broad_filter_map(async |(_shortstatekey, shorteventid)| { services .timeline .get_pdu_from_shorteventid(shorteventid) .ok() .await }) .ready_filter(|event| *event.kind() == TimelineEventType::RoomMember) .ready_filter(|event| { event .state_key() .is_some_and(|state_key| state_key != sender_user) }) .ready_filter_map(|event| { let content: RoomMemberEventContent = event.get_content().ok()?; let user_id: OwnedUserId = event.state_key()?.parse().ok()?; Some((content.membership, user_id)) }) .chain(joined_members_burst.stream()) .fold(lists, async |(mut changed, mut left), (membership, user_id)| { use MembershipState::*; let should_add = async |user_id| { !share_encrypted_room(services, sender_user, user_id, Some(room_id)).await }; match membership { | Join if should_add(&user_id).await => changed.insert(user_id), | Leave => left.insert(user_id), | _ => false, }; (changed, left) }) .map(Ok) .boxed() .await }