tuwunel_api_client/sync/v5/
extensions.rs1mod account_data;
2mod e2ee;
3mod profiles;
4mod receipts;
5mod to_device;
6mod typing;
7
8#[cfg(test)]
9mod tests;
10
11use std::{collections::BTreeMap, fmt::Debug};
12
13use futures::{FutureExt, future::join4};
14use ruma::{
15 OwnedRoomId, RoomId,
16 api::client::sync::sync_events::v5::{
17 ListId,
18 request::ExtensionRoomConfig,
19 response::{Extensions, Room as ResponseRoom},
20 },
21};
22use tuwunel_core::{Result, apply, at, extract_variant, implement, utils::BoolExt};
23use tuwunel_service::sync::Connection;
24
25pub(super) use self::to_device::{
26 acknowledge as acknowledge_to_device, check as check_to_device, pending as to_device_pending,
27};
28use self::{
29 account_data::{
30 collect as collect_account_data, collect_ranges as collect_account_data_ranges,
31 },
32 profiles::collect as collect_profiles,
33 receipts::collect_ranges as collect_receipt_ranges,
34};
35use super::{SyncInfo, Window, WindowRoom, joined_since, range::Results, share_encrypted_room};
36
37pub(super) struct Collected {
38 response: Extensions,
39 typing: typing::Collected,
40 keys_changed: bool,
42}
43
44#[tracing::instrument(
45 name = "extensions",
46 level = "debug",
47 skip_all,
48 fields(
49 next_batch = conn.next_batch,
50 window = window.len(),
51 rooms = conn.rooms.len(),
52 subs = conn.subscriptions.len(),
53 )
54)]
55pub(super) async fn handle(
56 sync_info: SyncInfo<'_>,
57 conn: &Connection,
58 window: &Window,
59) -> Result<Collected> {
60 let account_data = conn
61 .extensions
62 .account_data
63 .enabled
64 .unwrap_or(false)
65 .then_async(|| collect_account_data(sync_info, conn));
66
67 let typing = conn
68 .extensions
69 .typing
70 .enabled
71 .unwrap_or(false)
72 .then_async(|| typing::collect(sync_info, conn, window));
73
74 let to_device = conn
75 .extensions
76 .to_device
77 .enabled
78 .unwrap_or(false)
79 .then_async(|| to_device::collect(sync_info, conn));
80
81 let e2ee = conn
82 .extensions
83 .e2ee
84 .enabled
85 .unwrap_or(false)
86 .then_async(|| e2ee::collect(sync_info, conn));
87
88 let (account_data, typing, to_device, e2ee) = join4(account_data, typing, to_device, e2ee)
89 .map(apply!(4, |t: Option<_>| t.unwrap_or(Ok(Default::default()))))
90 .await;
91
92 let (e2ee, keys_changed) = e2ee?;
93
94 let response = Extensions {
97 account_data: account_data?,
98 receipts: Default::default(),
99 typing: Default::default(),
100 to_device: to_device?,
101 e2ee,
102 profiles: Default::default(),
103 };
104
105 Ok(Collected { response, typing: typing?, keys_changed })
106}
107
108#[implement(Collected)]
109#[inline]
110pub(super) fn keys_changed(&self) -> bool { self.keys_changed }
111
112#[implement(Collected)]
113pub(super) fn into_response(
114 mut self,
115 payloads: &BTreeMap<OwnedRoomId, ResponseRoom>,
116) -> Extensions {
117 self.response.typing = self.typing.into_response(payloads);
118 self.response
119}
120
121#[tracing::instrument(level = "trace", skip_all)]
122pub(super) async fn apply_profiles(
123 sync_info: SyncInfo<'_>,
124 conn: &Connection,
125 window: &Window,
126 ranges: &Results,
127 extensions: &mut Collected,
128) -> Result {
129 if conn.extensions.profiles.enabled.unwrap_or(false) {
130 extensions.response.profiles = collect_profiles(sync_info, conn, window, ranges).await?;
131 }
132
133 Ok(())
134}
135
136pub(super) fn apply_ranges(
137 conn: &Connection,
138 window: &Window,
139 ranges: &mut Results,
140 extensions: &mut Collected,
141) {
142 if conn.extensions.receipts.enabled.unwrap_or(false) {
143 extensions.response.receipts = collect_receipt_ranges(conn, window, ranges);
144 }
145
146 if conn
147 .extensions
148 .account_data
149 .enabled
150 .unwrap_or(false)
151 {
152 extensions.response.account_data.rooms =
153 collect_account_data_ranges(conn, window, ranges);
154 }
155}
156
157#[tracing::instrument(
158 name = "selector",
159 level = "trace",
160 skip_all,
161 fields(?implicit, ?explicit),
162)]
163fn selector<'a, ListIter, SubsIter>(
164 conn: &'a Connection,
165 window: &'a Window,
166 implicit: Option<ListIter>,
167 explicit: Option<SubsIter>,
168) -> impl Iterator<Item = &'a RoomId> + Send + Sync + 'a
169where
170 ListIter: Iterator<Item = &'a ListId> + Clone + Debug + Send + Sync + 'a,
171 SubsIter: Iterator<Item = &'a ExtensionRoomConfig> + Clone + Debug + Send + Sync + 'a,
172{
173 let has_all_subscribed = explicit
174 .clone()
175 .into_iter()
176 .flatten()
177 .any(|erc| matches!(erc, ExtensionRoomConfig::AllSubscribed));
178 let implicit_subscribed = implicit.clone();
179 let implicit_explicit = implicit.clone();
180
181 let all_subscribed = has_all_subscribed
182 .then(|| {
183 window
184 .keys()
185 .filter(|room_id| conn.subscriptions.contains_key(*room_id))
186 .filter(move |room_id| {
187 window
188 .get(*room_id)
189 .is_some_and(|room| !implicit_match(room, implicit_subscribed.as_ref()))
190 })
191 })
192 .into_iter()
193 .flatten()
194 .map(AsRef::as_ref);
195
196 let rooms_explicit = has_all_subscribed
197 .is_false()
198 .then(move || {
199 explicit
200 .into_iter()
201 .flatten()
202 .filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room))
203 .filter(move |room_id| {
204 window
205 .get::<RoomId>(room_id.as_ref())
206 .is_some_and(|room| !implicit_match(room, implicit_explicit.as_ref()))
207 })
208 .map(AsRef::as_ref)
209 })
210 .into_iter()
211 .flatten();
212
213 let rooms_selected = window
214 .iter()
215 .filter(move |(_, room)| implicit_match(room, implicit.as_ref()))
216 .map(at!(0))
217 .map(AsRef::as_ref);
218
219 all_subscribed
220 .chain(rooms_explicit)
221 .chain(rooms_selected)
222 .filter(|room_id| {
223 window
224 .get(*room_id)
225 .is_some_and(|room| !room.forgotten)
226 })
227}
228
229fn implicit_match<'a, ListIter>(room: &WindowRoom, implicit: Option<&ListIter>) -> bool
230where
231 ListIter: Iterator<Item = &'a ListId> + Clone,
232{
233 implicit.is_none_or(|lists| {
234 lists
235 .clone()
236 .any(|list| room.lists.contains(list))
237 })
238}