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