1use std::{collections::BTreeMap, 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, Room, RoomConfig},
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::client::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<RoomConfig>,
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 = (&RoomId, Option<RoomConfig>)> {
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 payload = 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 let public_receipts = public_receipts(sync_info, conn, room_id, room.roomsince, ignored)
184 .map_err(|error| Failure::new(Domain::PublicReceipt, error));
185
186 let private_receipts = private_receipts(sync_info, conn, room_id, room.roomsince)
187 .map_err(|error| Failure::new(Domain::PrivateRead, error));
188
189 let account_data = room_account_data(sync_info, conn, room_id, room.roomsince)
190 .map_err(|error| Failure::new(Domain::RoomAccountData, error));
191
192 let (payload, public_receipts, private_receipts, account_data) =
193 try_join4(payload, public_receipts, private_receipts, account_data).await?;
194
195 let required_state = membership_allows_required_state(window_room.membership.as_ref())
196 .then_some(required_state)
197 .unwrap_or_default();
198
199 assemble(
200 payload,
201 public_receipts,
202 private_receipts,
203 account_data,
204 (config_hash, required_state),
205 )
206}
207
208async fn public_receipts(
209 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
210 conn: &Connection,
211 room_id: &RoomId,
212 roomsince: u64,
213 ignored: &OnceCell<Option<IgnoredUserListEvent>>,
214) -> Result<impl Iterator<Item = Raw<AnySyncEphemeralRoomEvent>>> {
215 let mut receipts: Vec<(OwnedUserId, Raw<AnySyncEphemeralRoomEvent>)> = services
216 .read_receipt
217 .readreceipts_since_fallible(room_id, roomsince, Some(conn.next_batch))
218 .map_ok(|(user_id, _ts, event)| (user_id.to_owned(), event))
219 .try_collect()
220 .await?;
221
222 if !receipts.is_empty() {
223 let ignored = ignored
224 .get_or_try_init(async || {
225 services
226 .account_data
227 .get_global(sender_user, GlobalAccountDataEventType::IgnoredUserList)
228 .await
229 .optional()
230 })
231 .await?;
232
233 if let Some(ignored) = ignored {
234 receipts.retain(|(user_id, _)| {
235 !ignored
236 .content
237 .ignored_users
238 .contains_key(user_id)
239 });
240 }
241 }
242
243 Ok(receipts.into_iter().map(at!(1)))
244}
245
246async fn private_receipts(
247 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
248 conn: &Connection,
249 room_id: &RoomId,
250 roomsince: u64,
251) -> Result<PrivateReadEvents> {
252 let update = services
253 .read_receipt
254 .last_privateread_update_fallible(sender_user, room_id)
255 .await?;
256
257 match update {
258 | _ if update <= roomsince => Ok(PrivateReadEvents::new()),
259 | _ if update > conn.next_batch =>
260 Err(err!(Database("Private read advanced beyond the bounded sync range."))),
261 | _ =>
262 services
263 .read_receipt
264 .private_read_get_fallible(room_id, sender_user, update)
265 .await,
266 }
267}
268
269async fn room_account_data(
270 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
271 conn: &Connection,
272 room_id: &RoomId,
273 roomsince: u64,
274) -> Result<Vec<Raw<AnyRoomAccountDataEvent>>> {
275 services
276 .account_data
277 .changes_since_fallible(Some(room_id), sender_user, roomsince, Some(conn.next_batch))
278 .ready_try_filter_map(|event| Ok(extract_variant!(event, AnyRawAccountDataEvent::Room)))
279 .ready_try_filter(move |event| roomsince != 0 || !is_empty_account_data_event(event))
280 .try_collect()
281 .await
282}
283
284fn assemble<PublicReceipts>(
285 payload: Option<response::Room>,
286 public_receipts: PublicReceipts,
287 private_receipts: PrivateReadEvents,
288 account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
289 config: RoomConfig,
290) -> Result<CompleteRange, Failure>
291where
292 PublicReceipts: Iterator<Item = Raw<AnySyncEphemeralRoomEvent>>,
293{
294 let mut receipts = public_receipts.chain(private_receipts).peekable();
295 let receipts = receipts
296 .peek()
297 .is_some()
298 .then(|| pack_receipts_fallible(receipts))
299 .transpose()
300 .map_err(|error| Failure::new(Domain::ReceiptSerialization, error))?;
301
302 let config = payload.as_ref().map(|_| config);
303
304 Ok(CompleteRange { payload, config, receipts, account_data })
305}
306
307#[cfg(test)]
308mod tests {
309 use std::iter::once;
310
311 use ruma::{api::client::sync::sync_events::v5::response::Room as ResponseRoom, room_id};
312 use serde_json::{json, value::to_raw_value};
313
314 use super::*;
315
316 #[test]
317 fn malformed_receipt_withholds_the_complete_range() {
318 let room_id = room_id!("!receipt:example.com");
319 let malformed = Raw::from_json(
320 to_raw_value(&json!({"content": 5})).expect("test JSON should serialize"),
321 );
322
323 let range = assemble(
324 Some(ResponseRoom::default()),
325 vec![malformed].into_iter(),
326 PrivateReadEvents::new(),
327 Vec::new(),
328 (7, once(11).collect()),
329 );
330
331 let error = range.expect_err("malformed receipt must fail the complete range");
332
333 assert_eq!(error.domain, Domain::ReceiptSerialization);
334 assert!(
335 !publish(room_id, Err(error))
336 .ranges
337 .contains_key(room_id)
338 );
339 }
340
341 #[test]
342 fn extension_only_range_commits_without_a_room_payload() {
343 let room_id = room_id!("!extension-only:example.com");
344 let range = assemble(
345 None,
346 Vec::new().into_iter(),
347 PrivateReadEvents::new(),
348 Vec::new(),
349 (7, once(11).collect()),
350 );
351
352 let mut range = publish(room_id, range); assert_eq!(range.room_updates().collect::<Vec<_>>(), [(room_id, None)]);
355 assert!(range.into_payloads().is_empty());
356 }
357
358 #[test]
359 fn extension_outputs_are_taken_once_without_removing_the_complete_range() {
360 let room_id = room_id!("!extension-output:example.com");
361 let receipt = Raw::from_json(
362 to_raw_value(&json!({"content": {}})).expect("test receipt should serialize"),
363 );
364
365 let account_data = Raw::from_json(
366 to_raw_value(&json!({"type": "m.tag", "content": {"tags": {}}}))
367 .expect("test account data should serialize"),
368 );
369
370 let range = CompleteRange {
371 payload: None,
372 config: None,
373 receipts: Some(receipt),
374 account_data: vec![account_data],
375 };
376
377 let ranges = [(room_id.to_owned(), range)].into();
378 let mut results = Results { ranges };
379
380 assert!(results.take_receipts(room_id).is_some());
381 assert!(results.take_receipts(room_id).is_none());
382
383 let count = results
384 .take_account_data(room_id)
385 .map(|events| events.len());
386
387 assert_eq!(Some(1), count);
388 assert!(results.take_account_data(room_id).is_none());
389 assert!(results.ranges.contains_key(room_id));
390 }
391
392 fn publish(room_id: &RoomId, range: Result<CompleteRange, Failure>) -> Results {
393 let ranges = range
394 .ok()
395 .map(|range| (room_id.to_owned(), range))
396 .into_iter()
397 .collect();
398
399 Results { ranges }
400 }
401}