1use std::{collections::BTreeMap, iter::empty, mem::take};
2
3use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join4};
4use ruma::{
5 OwnedRoomId, OwnedUserId, RoomId,
6 api::client::sync::sync_events::v5::response,
7 events::{
8 AnyRawAccountDataEvent, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent,
9 GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent,
10 receipt::SyncReceiptEvent,
11 },
12 serde::Raw,
13};
14use tokio::sync::OnceCell;
15use tuwunel_core::{
16 Error, Result, at, err, error, extract_variant, implement,
17 result::NotFound,
18 utils::{BoolExt, IterStream, TryReadyExt, stream::BroadbandExt},
19};
20use tuwunel_service::{
21 rooms::read_receipt::{PrivateReadEvents, pack_receipts_fallible},
22 sync::{Connection, DeliveredConfig, Room, RoomUpdate},
23};
24
25use super::{
26 SyncInfo, Window, WindowRoom,
27 rooms::{
28 Failure as RoomFailure,
29 Failure::{Payload as PayloadFailure, Timeline as TimelineFailure},
30 RoomDetails, handle_room, membership_allows_required_state, merged_room_details,
31 room_config,
32 },
33};
34use crate::is_empty_account_data_event;
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37enum Domain {
38 Timeline,
39 Payload,
40 PublicReceipt,
41 PrivateRead,
42 RoomAccountData,
43 ReceiptSerialization,
44}
45
46#[derive(Debug)]
47struct Failure {
48 domain: Domain,
49 error: Error,
50}
51
52impl Failure {
53 fn new(domain: Domain, error: Error) -> Self { Self { domain, error } }
54}
55
56impl From<RoomFailure> for Failure {
57 fn from(failure: RoomFailure) -> Self {
58 match failure {
59 | TimelineFailure(error) => Self::new(Domain::Timeline, error),
60 | PayloadFailure(error) => Self::new(Domain::Payload, error),
61 }
62 }
63}
64
65#[derive(Debug)]
66struct CompleteRange {
67 payload: Option<response::Room>,
68 config: Option<DeliveredConfig>,
69 receipts: Option<Raw<SyncReceiptEvent>>,
70 account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
71}
72
73#[derive(Default)]
74pub(super) struct Results {
75 ranges: BTreeMap<OwnedRoomId, CompleteRange>,
76}
77
78#[implement(Results)]
79pub(super) fn room_updates(&mut self) -> impl Iterator<Item = RoomUpdate<'_>> {
80 self.ranges
81 .iter_mut()
82 .map(|(room_id, range)| (room_id.as_ref(), range.config.take()))
83}
84
85#[implement(Results)]
86pub(super) fn payload(&self, room_id: &RoomId) -> Option<&response::Room> {
87 self.ranges
88 .get(room_id)
89 .and_then(|range| range.payload.as_ref())
90}
91
92#[implement(Results)]
93pub(super) fn into_payloads(self) -> BTreeMap<OwnedRoomId, response::Room> {
94 self.ranges
95 .into_iter()
96 .filter_map(|(room_id, range)| range.payload.map(|payload| (room_id, payload)))
97 .collect()
98}
99
100#[implement(Results)]
101pub(super) fn take_receipts(&mut self, room_id: &RoomId) -> Option<Raw<SyncReceiptEvent>> {
102 self.ranges
103 .get_mut(room_id)
104 .and_then(|range| range.receipts.take())
105}
106
107#[implement(Results)]
108pub(super) fn take_account_data(
109 &mut self,
110 room_id: &RoomId,
111) -> Option<Vec<Raw<AnyRoomAccountDataEvent>>> {
112 self.ranges
113 .get_mut(room_id)
114 .map(|range| take(&mut range.account_data))
115 .filter(|events| !events.is_empty())
116}
117
118#[tracing::instrument(
119 name = "ranges",
120 level = "debug",
121 skip_all,
122 fields(
123 next_batch = conn.next_batch,
124 window = window.len(),
125 ),
126)]
127pub(super) async fn collect(
128 sync_info: SyncInfo<'_>,
129 conn: &Connection,
130 window: &Window,
131) -> Results {
132 let ignored = OnceCell::new();
133 let empty_room = Room::default();
134 let ranges = window
135 .iter()
136 .stream()
137 .broad_filter_map(async |(room_id, window_room)| {
138 let room = conn.rooms.get(room_id).unwrap_or(&empty_room);
139
140 let room_details = merged_room_details(conn, &window_room.lists, room_id);
141
142 match collect_room(sync_info, conn, window_room, room, room_details, &ignored).await {
143 | Ok(range) => Some((room_id.clone(), range)),
144 | Err(Failure { domain, error }) => {
145 error!(
146 %room_id,
147 ?domain,
148 roomsince = room.roomsince,
149 next_batch = conn.next_batch,
150 %error,
151 "sliding sync range failed"
152 );
153 None
154 },
155 }
156 })
157 .collect()
158 .await;
159
160 Results { ranges }
161}
162
163async fn collect_room(
164 sync_info: SyncInfo<'_>,
165 conn: &Connection,
166 window_room: &WindowRoom,
167 room: &Room,
168 room_details: RoomDetails,
169 ignored: &OnceCell<Option<IgnoredUserListEvent>>,
170) -> Result<CompleteRange, Failure> {
171 let room_id = &window_room.room_id;
172 let (config_hash, required_state) = room_config(&room_details);
173 let config_changed = room.config_hash != config_hash;
174 let payload_is_fresh = window_room.payload_is_fresh(room.roomsince) || config_changed;
175
176 let delivery = payload_is_fresh
177 .then_async(|| {
178 handle_room(sync_info, conn, window_room, room, config_changed, room_details)
179 })
180 .map(Option::transpose)
181 .map_err(Failure::from);
182
183 if window_room.forgotten {
184 let (payload, state_read) = delivery.map_ok(Option::unzip).await?;
185 let (shortstatehash, unread_state) = state_read.unwrap_or_default();
186 let shortstatehash = shortstatehash.unwrap_or(room.shortstatehash);
187
188 return assemble(
189 payload,
190 empty(),
191 PrivateReadEvents::new(),
192 Vec::new(),
193 (config_hash, Default::default(), shortstatehash, unread_state),
194 );
195 }
196
197 let public_receipts = public_receipts(sync_info, conn, room_id, room.roomsince, ignored)
198 .map_err(|error| Failure::new(Domain::PublicReceipt, error));
199
200 let private_receipts = private_receipts(sync_info, conn, room_id, room.roomsince)
201 .map_err(|error| Failure::new(Domain::PrivateRead, error));
202
203 let account_data = room_account_data(sync_info, conn, room_id, room.roomsince)
204 .map_err(|error| Failure::new(Domain::RoomAccountData, error));
205
206 let (delivery, public_receipts, private_receipts, account_data) =
207 try_join4(delivery, public_receipts, private_receipts, account_data).await?;
208
209 let required_state = membership_allows_required_state(window_room.membership.as_ref())
210 .then_some(required_state)
211 .unwrap_or_default();
212
213 let (payload, state_read) = delivery.unzip();
215 let (shortstatehash, unread_state) = state_read.unwrap_or_default();
216 let shortstatehash = shortstatehash.unwrap_or(room.shortstatehash);
217
218 assemble(
219 payload,
220 public_receipts,
221 private_receipts,
222 account_data,
223 (config_hash, required_state, shortstatehash, unread_state),
224 )
225}
226
227async fn public_receipts(
228 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
229 conn: &Connection,
230 room_id: &RoomId,
231 roomsince: u64,
232 ignored: &OnceCell<Option<IgnoredUserListEvent>>,
233) -> Result<impl Iterator<Item = Raw<AnySyncEphemeralRoomEvent>>> {
234 let mut receipts: Vec<(OwnedUserId, Raw<AnySyncEphemeralRoomEvent>)> = services
235 .read_receipt
236 .readreceipts_since_fallible(room_id, roomsince, Some(conn.next_batch))
237 .map_ok(|(user_id, _ts, event)| (user_id.to_owned(), event))
238 .try_collect()
239 .await?;
240
241 if !receipts.is_empty() {
242 let ignored = ignored
243 .get_or_try_init(async || {
244 services
245 .account_data
246 .get_global(sender_user, GlobalAccountDataEventType::IgnoredUserList)
247 .await
248 .optional()
249 })
250 .await?;
251
252 if let Some(ignored) = ignored {
253 receipts.retain(|(user_id, _)| {
254 !ignored
255 .content
256 .ignored_users
257 .contains_key(user_id)
258 });
259 }
260 }
261
262 Ok(receipts.into_iter().map(at!(1)))
263}
264
265async fn private_receipts(
266 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
267 conn: &Connection,
268 room_id: &RoomId,
269 roomsince: u64,
270) -> Result<PrivateReadEvents> {
271 let update = services
272 .read_receipt
273 .last_privateread_update_fallible(sender_user, room_id)
274 .await?;
275
276 match update {
277 | _ if update <= roomsince => Ok(PrivateReadEvents::new()),
278 | _ if update > conn.next_batch =>
279 Err(err!(Database("Private read advanced beyond the bounded sync range."))),
280 | _ =>
281 services
282 .read_receipt
283 .private_read_get_fallible(room_id, sender_user, update)
284 .await,
285 }
286}
287
288async fn room_account_data(
289 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
290 conn: &Connection,
291 room_id: &RoomId,
292 roomsince: u64,
293) -> Result<Vec<Raw<AnyRoomAccountDataEvent>>> {
294 services
295 .account_data
296 .changes_since_fallible(Some(room_id), sender_user, roomsince, Some(conn.next_batch))
297 .ready_try_filter_map(|event| Ok(extract_variant!(event, AnyRawAccountDataEvent::Room)))
298 .ready_try_filter(move |event| roomsince != 0 || !is_empty_account_data_event(event))
299 .try_collect()
300 .await
301}
302
303fn assemble<PublicReceipts>(
304 payload: Option<response::Room>,
305 public_receipts: PublicReceipts,
306 private_receipts: PrivateReadEvents,
307 account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
308 config: DeliveredConfig,
309) -> Result<CompleteRange, Failure>
310where
311 PublicReceipts: Iterator<Item = Raw<AnySyncEphemeralRoomEvent>>,
312{
313 let mut receipts = public_receipts.chain(private_receipts).peekable();
314 let receipts = receipts
315 .peek()
316 .is_some()
317 .then(|| pack_receipts_fallible(receipts))
318 .transpose()
319 .map_err(|error| Failure::new(Domain::ReceiptSerialization, error))?;
320
321 let config = payload.as_ref().map(|_| config);
322
323 Ok(CompleteRange { payload, config, receipts, account_data })
324}
325
326#[cfg(test)]
327mod tests {
328 use std::iter::once;
329
330 use ruma::{api::client::sync::sync_events::v5::response::Room as ResponseRoom, room_id};
331 use serde_json::{json, value::to_raw_value};
332
333 use super::*;
334
335 #[test]
336 fn malformed_receipt_withholds_the_complete_range() {
337 let room_id = room_id!("!receipt:example.com");
338 let malformed = Raw::from_json(
339 to_raw_value(&json!({"content": 5})).expect("test JSON should serialize"),
340 );
341
342 let range = assemble(
343 Some(ResponseRoom::default()),
344 vec![malformed].into_iter(),
345 PrivateReadEvents::new(),
346 Vec::new(),
347 (7, once(11).collect(), 13, Default::default()),
348 );
349
350 let error = range.expect_err("malformed receipt must fail the complete range");
351
352 assert_eq!(error.domain, Domain::ReceiptSerialization);
353 assert!(
354 !publish(room_id, Err(error))
355 .ranges
356 .contains_key(room_id)
357 );
358 }
359
360 #[test]
361 fn extension_only_range_commits_without_a_room_payload() {
362 let room_id = room_id!("!extension-only:example.com");
363 let range = assemble(
364 None,
365 Vec::new().into_iter(),
366 PrivateReadEvents::new(),
367 Vec::new(),
368 (7, once(11).collect(), 13, Default::default()),
369 );
370
371 let mut range = publish(room_id, range); assert_eq!(range.room_updates().collect::<Vec<_>>(), [(room_id, None)]);
374 assert!(range.into_payloads().is_empty());
375 }
376
377 #[test]
378 fn extension_outputs_are_taken_once_without_removing_the_complete_range() {
379 let room_id = room_id!("!extension-output:example.com");
380 let receipt = Raw::from_json(
381 to_raw_value(&json!({"content": {}})).expect("test receipt should serialize"),
382 );
383
384 let account_data = Raw::from_json(
385 to_raw_value(&json!({"type": "m.tag", "content": {"tags": {}}}))
386 .expect("test account data should serialize"),
387 );
388
389 let range = CompleteRange {
390 payload: None,
391 config: None,
392 receipts: Some(receipt),
393 account_data: vec![account_data],
394 };
395
396 let ranges = [(room_id.to_owned(), range)].into();
397 let mut results = Results { ranges };
398
399 assert!(results.take_receipts(room_id).is_some());
400 assert!(results.take_receipts(room_id).is_none());
401
402 let count = results
403 .take_account_data(room_id)
404 .map(|events| events.len());
405
406 assert_eq!(Some(1), count);
407 assert!(results.take_account_data(room_id).is_none());
408 assert!(results.ranges.contains_key(room_id));
409 }
410
411 fn publish(room_id: &RoomId, range: Result<CompleteRange, Failure>) -> Results {
412 let ranges = range
413 .ok()
414 .map(|range| (room_id.to_owned(), range))
415 .into_iter()
416 .collect();
417
418 Results { ranges }
419 }
420}