Skip to main content

tuwunel_api_client/sync/v5/
extensions.rs

1mod 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	/// Whether the one-time-key counts changed since the connection's position.
41	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	// Receipt and room account-data payloads only exist as bounded room-range
95	// outputs, applied by `apply_ranges` after the ranges resolve.
96	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}