1use std::collections::BTreeMap;
2
3use futures::{
4 FutureExt, StreamExt, TryFutureExt,
5 future::{join3, join4},
6};
7use ruma::{
8 OwnedEventId, OwnedRoomId, RoomId, UserId,
9 api::client::{
10 filter::FilterDefinition,
11 sync::sync_events::v3::{LeftRoom, RoomAccountData, StateEvents, Timeline},
12 },
13 events::{AnyRawAccountDataEvent, TimelineEventType::*, room::member::MembershipState},
14};
15use tuwunel_core::{
16 Error, Result, at, debug,
17 error::inspect_debug_log,
18 extract_variant, is_equal_to, ref_at,
19 result::FlatOk,
20 smallvec::SmallVec,
21 utils::{
22 BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
23 future::ReadyBoolExt,
24 option::OptionExt,
25 result::MapExpect,
26 stream::{BroadbandExt, WidebandExt},
27 },
28 warn,
29};
30use tuwunel_matrix::{
31 Event,
32 event::{Matches, trim_event_fields},
33 pdu::{PduCount, PduEvent},
34};
35use tuwunel_service::Services;
36
37use super::{
38 super::{
39 load_timeline, own_departure, stand_in_leave, strip_state_unsigned, timeline_prev_batch,
40 },
41 StateAfter, StateChangeParams, StateChanges, missing_room_state,
42 state::calculate_state_changes,
43};
44use crate::{ignored_filter, is_empty_account_data_event, with_membership};
45
46type TimelineEventIds = SmallVec<[OwnedEventId; 1]>;
47
48pub(super) fn collect_rooms<'a>(
49 services: &'a Services,
50 sender_user: &'a UserId,
51 since: u64,
52 next_batch: u64,
53 full_state: bool,
54 state_after: StateAfter,
55 filter: &'a FilterDefinition,
56) -> impl Future<Output = BTreeMap<OwnedRoomId, LeftRoom>> + Send + 'a {
57 services
58 .state_cache
59 .rooms_departed(sender_user)
60 .ready_filter(|room_id| filter.room.matches(room_id))
61 .map(ToOwned::to_owned)
62 .broad_filter_map(move |room_id| {
63 handle_left_room(
64 services,
65 since,
66 room_id.clone(),
67 sender_user,
68 next_batch,
69 full_state,
70 state_after,
71 filter,
72 )
73 .map_ok(move |left_room| (room_id, left_room))
74 .ok()
75 })
76 .ready_filter_map(|(room_id, left_room)| left_room.map(|left_room| (room_id, left_room)))
77 .collect()
78}
79
80#[tracing::instrument(
81 target = "tuwunel_api_client::sync::v3",
82 name = "left",
83 level = "debug",
84 skip_all,
85 fields(
86 room_id = %room_id,
87 full = %full_state,
88 ),
89)]
90#[expect(clippy::too_many_arguments)]
91async fn handle_left_room(
92 services: &Services,
93 since: u64,
94 ref room_id: OwnedRoomId,
95 sender_user: &UserId,
96 next_batch: u64,
97 full_state: bool,
98 state_after: StateAfter,
99 filter: &FilterDefinition,
100) -> Result<Option<LeftRoom>> {
101 let left_count = services
102 .state_cache
103 .get_departure_count(room_id, sender_user)
104 .await
105 .unwrap_or(0);
106
107 if left_count == 0 || left_count > next_batch {
108 return Ok(None);
109 }
110
111 let include_leave = filter.room.include_leave;
112 if since == 0 && !include_leave {
113 return Ok(None);
114 }
115
116 if since != 0 && left_count <= since {
119 return Ok(None);
120 }
121
122 let forgotten = services
123 .state_cache
124 .is_forgotten(sender_user, room_id)
125 .await?;
126
127 if forgotten && since == 0 {
128 return Ok(None);
129 }
130
131 let is_not_found = services.metadata.exists(room_id).is_false();
132
133 let is_disabled = services.metadata.is_disabled(room_id);
134
135 let is_banned = services.metadata.is_banned(room_id);
136
137 let is_out_of_band = departed_out_of_band(services, room_id, sender_user, since);
139
140 if forgotten
141 || is_not_found
142 .or3(is_disabled, is_banned, is_out_of_band)
143 .await
144 {
145 let departure = forgotten
146 .then_async(async || {
147 own_departure(services, room_id, sender_user)
148 .await
149 .map(Event::into_format)
150 })
151 .await
152 .flatten();
153
154 let event =
155 departure.map_or_else(|| stand_in_leave(services, room_id, sender_user), Ok)?;
156
157 let state = state_after.wrap(StateEvents {
158 events: vec![trim_event_fields(event, filter.event_fields.as_deref())],
159 });
160
161 return Ok(Some(LeftRoom {
162 account_data: RoomAccountData::default(),
163 state,
164 timeline: Timeline {
165 limited: false,
166 events: Default::default(),
167 prev_batch: Some(left_count.to_string()),
168 },
169 }));
170 }
171
172 let horizon = services
173 .state_accessor
174 .departure_count(sender_user, room_id)
175 .await
176 .inspect_err(|error| warn!(target: "tuwunel_api_client::sync::v3", %room_id, %sender_user, %error, "Unreadable departure count"))
177 .ok()
178 .flatten()
179 .map(PduCount::into_unsigned)
180 .unwrap_or(left_count);
181
182 load_left_room(
183 services,
184 sender_user,
185 room_id,
186 since,
187 horizon,
188 full_state,
189 state_after,
190 filter,
191 )
192 .await
193}
194
195async fn departed_out_of_band(
205 services: &Services,
206 room_id: &RoomId,
207 sender_user: &UserId,
208 since: u64,
209) -> bool {
210 let no_later_event = since != 0
211 && services
212 .timeline
213 .last_timeline_count(None, room_id, None)
214 .await
215 .inspect_err(|error| warn!(target: "tuwunel_api_client::sync::v3", %room_id, %since, %error, "Unreadable last event count"))
216 .is_ok_and(|last| last <= PduCount::Normal(since));
217
218 own_departure(services, room_id, sender_user)
219 .await
220 .is_none_or(|event| {
221 no_later_event && event.membership_for(sender_user) != Some(MembershipState::Ban)
222 })
223}
224
225#[tracing::instrument(
226 target = "tuwunel_api_client::sync::v3",
227 name = "load",
228 level = "debug",
229 skip_all
230)]
231#[expect(clippy::too_many_arguments)]
232async fn load_left_room(
233 services: &Services,
234 sender_user: &UserId,
235 room_id: &RoomId,
236 since: u64,
237 horizon: u64,
238 full_state: bool,
239 state_after: StateAfter,
240 filter: &FilterDefinition,
241) -> Result<Option<LeftRoom>> {
242 let initial = since == 0;
243 let timeline_limit: usize = filter
244 .room
245 .timeline
246 .limit
247 .map(TryInto::try_into)
248 .map_expect("UInt to usize")
249 .unwrap_or(10)
250 .min(100);
251
252 let (timeline_pdus, limited, _) = load_timeline(
253 services,
254 sender_user,
255 room_id,
256 PduCount::Normal(since),
257 Some(PduCount::Normal(horizon)),
258 timeline_limit.max(1),
259 )
260 .await
261 .unwrap_or_default();
262
263 let since_shortstatehash = since
264 .lt(&horizon)
265 .then_async(|| {
266 services
267 .timeline
268 .next_shortstatehash(room_id, PduCount::Normal(since))
269 .ok()
270 })
271 .map(Option::flatten);
272
273 let horizon_shortstatehash = timeline_pdus
274 .first()
275 .map(at!(0))
276 .map_async(|count| {
277 services
278 .timeline
279 .get_shortstatehash(room_id, count)
280 .inspect_err(log_horizon_error)
281 .ok()
282 });
283
284 let after_shortstatehash = state_after.requested().then_async(|| {
286 services
287 .timeline
288 .shortstatehash_after(room_id, PduCount::Normal(horizon))
289 .inspect_err(inspect_debug_log)
290 });
291
292 let left_shortstatehash = services
293 .timeline
294 .get_shortstatehash(room_id, PduCount::Normal(horizon))
295 .inspect_err(inspect_debug_log)
296 .or_else(|_| services.state.get_room_shortstatehash(room_id))
297 .map_err(|_| missing_room_state(room_id));
298
299 let (since_shortstatehash, horizon_shortstatehash, after_shortstatehash, left_shortstatehash) =
300 join4(
301 since_shortstatehash,
302 horizon_shortstatehash,
303 after_shortstatehash,
304 left_shortstatehash,
305 )
306 .boxed() .await;
308
309 let StateChanges { state_events, .. } =
310 calculate_state_changes(services, sender_user, room_id, StateChangeParams {
311 full_state: full_state || initial,
312 state_after,
313 since_shortstatehash,
314 horizon_shortstatehash: horizon_shortstatehash.flatten(),
315 after_shortstatehash: after_shortstatehash.flat_ok(),
316 current_shortstatehash: left_shortstatehash?,
317 joined_since_last_sync: false,
318 witness: None,
319 include_heroes: true,
320 })
321 .await?;
322
323 let is_sender_membership = |event: &PduEvent| {
324 *event.kind() == RoomMember && event.state_key() == Some(sender_user.as_str())
325 };
326
327 let departure = late_departure(services, sender_user, room_id, since, horizon).await;
328
329 let has_departure = departure.is_some();
330
331 let timeline_sender_member = timeline_limit
332 .eq(&0)
333 .then(|| timeline_pdus.last().map(ref_at!(1)).cloned())
334 .into_iter()
335 .flat_map(Option::into_iter);
336
337 let encrypted = services
338 .state_accessor
339 .is_encrypted_room(room_id)
340 .await;
341
342 let event_fields = filter.event_fields.as_deref();
343
344 let in_timeline = in_timeline(&timeline_pdus);
345
346 let state_events = state_events
347 .into_iter()
348 .filter(|pdu| !has_departure || !is_sender_membership(pdu))
349 .chain(departure)
350 .filter(|pdu| filter.room.state.matches(pdu))
351 .filter(|pdu| timeline_limit > 0 || has_departure || !is_sender_membership(pdu))
352 .chain(timeline_sender_member)
353 .stream()
354 .wide_then(|pdu| with_membership(services, pdu, sender_user, encrypted))
355 .map(|pdu| strip_state_unsigned(pdu, sender_user, &in_timeline))
356 .map(|pdu| trim_event_fields(pdu.into_format(), event_fields))
357 .collect();
358
359 let left_prev_batch = timeline_limit
360 .eq(&0)
361 .then_some(horizon)
362 .map(PduCount::Normal);
363
364 let prev_batch = timeline_prev_batch(&timeline_pdus, PduCount::Normal(horizon))
365 .filter(|_| timeline_limit > 0)
366 .or(left_prev_batch)
367 .as_ref()
368 .map(ToString::to_string);
369
370 let timeline_events = timeline_pdus
371 .into_iter()
372 .stream()
373 .wide_filter_map(|item| ignored_filter(services, item, sender_user))
374 .map(at!(1))
375 .ready_filter(|pdu| filter.room.timeline.matches(pdu))
376 .take(timeline_limit)
377 .wide_then(|pdu| with_membership(services, pdu, sender_user, encrypted))
378 .wide_then(|pdu| {
379 services
380 .pdu_metadata
381 .bundle_aggregations(sender_user, pdu)
382 })
383 .collect::<Vec<_>>();
384
385 let account_data_events = services
386 .account_data
387 .changes_since(Some(room_id), sender_user, since, None)
388 .ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
389 .ready_filter(move |e| since != 0 || !is_empty_account_data_event(e))
390 .collect();
391
392 let (state_events, account_data_events, timeline_events) =
393 join3(state_events, account_data_events, timeline_events).await;
394
395 let state = state_after.wrap(StateEvents { events: state_events });
396
397 Ok(Some(LeftRoom {
398 account_data: RoomAccountData { events: account_data_events },
399 state,
400 timeline: Timeline {
401 prev_batch,
402 limited: limited || timeline_limit == 0,
403 events: timeline_events
404 .into_iter()
405 .map(|pdu| trim_event_fields(pdu.into_format(), event_fields))
406 .collect(),
407 },
408 }))
409}
410
411async fn late_departure(
412 services: &Services,
413 sender_user: &UserId,
414 room_id: &RoomId,
415 since: u64,
416 horizon: u64,
417) -> Option<PduEvent> {
418 since
419 .ge(&horizon)
420 .then_async(|| {
421 services
422 .timeline
423 .get_pdu_at(room_id, PduCount::Normal(horizon))
424 .inspect_err(inspect_debug_log)
425 .ok()
426 })
427 .map(|event| {
428 event.flatten().filter(|event| {
429 matches!(
430 event.membership_for(sender_user),
431 Some(MembershipState::Leave | MembershipState::Ban)
432 )
433 })
434 })
435 .await
436}
437
438pub(super) fn log_horizon_error(error: &Error) {
445 match error.is_not_found() {
446 | true =>
447 debug!(target: "tuwunel_api_client::sync::v3", ?error, "no shortstatehash at the window's first event"),
448 | false => inspect_debug_log(error),
449 }
450}
451
452pub(super) fn in_timeline(
453 timeline_pdus: &[(PduCount, PduEvent)],
454) -> impl Fn(&PduEvent) -> bool + use<> {
455 let timeline_ids: TimelineEventIds = timeline_pdus
456 .iter()
457 .map(ref_at!(1))
458 .map(Event::event_id)
459 .map(ToOwned::to_owned)
460 .collect();
461
462 move |event: &PduEvent| {
463 timeline_ids
464 .iter()
465 .any(is_equal_to!(event.event_id()))
466 }
467}