Skip to main content

tuwunel_api_client/sync/
mod.rs

1mod profiles;
2#[cfg(test)]
3mod tests;
4mod v3;
5mod v5;
6
7use futures::{StreamExt, pin_mut};
8use ruma::{
9	EventId, OwnedUserId, RoomId, UserId,
10	events::{
11		AnyStrippedStateEvent, AnySyncStateEvent, StateEventType,
12		TimelineEventType::{RoomCreate, RoomMember},
13		room::member::MembershipState,
14	},
15	serde::Raw,
16	uint,
17};
18use tuwunel_core::{
19	Error, Result, debug_warn, is_equal_to,
20	utils::{
21		result::{LogErr, NotFound},
22		time::now_millis,
23	},
24	warn,
25};
26use tuwunel_matrix::{
27	Event, PduCount,
28	pdu::{EventHash, PduEvent},
29};
30use tuwunel_service::{Services, users::InviteFilter};
31
32pub use self::v3::calculate_heroes;
33pub(crate) use self::{v3::sync_events_route, v5::sync_events_v5_route};
34
35#[derive(Clone, Copy)]
36enum TimelineErrors {
37	Ignore,
38	Propagate,
39}
40
41async fn load_timeline(
42	services: &Services,
43	sender_user: &UserId,
44	room_id: &RoomId,
45	roomsincecount: PduCount,
46	next_batch: Option<PduCount>,
47	limit: usize,
48) -> Result<(Vec<(PduCount, PduEvent)>, bool, PduCount), Error> {
49	load_timeline_with_errors(
50		services,
51		sender_user,
52		room_id,
53		roomsincecount,
54		next_batch,
55		limit,
56		TimelineErrors::Ignore,
57	)
58	.await
59}
60
61async fn load_timeline_fallible(
62	services: &Services,
63	sender_user: &UserId,
64	room_id: &RoomId,
65	roomsincecount: PduCount,
66	next_batch: Option<PduCount>,
67	limit: usize,
68) -> Result<(Vec<(PduCount, PduEvent)>, bool, PduCount), Error> {
69	load_timeline_with_errors(
70		services,
71		sender_user,
72		room_id,
73		roomsincecount,
74		next_batch,
75		limit,
76		TimelineErrors::Propagate,
77	)
78	.await
79}
80
81async fn load_timeline_with_errors(
82	services: &Services,
83	sender_user: &UserId,
84	room_id: &RoomId,
85	roomsincecount: PduCount,
86	next_batch: Option<PduCount>,
87	limit: usize,
88	errors: TimelineErrors,
89) -> Result<(Vec<(PduCount, PduEvent)>, bool, PduCount), Error> {
90	let until = next_batch.map(|count| count.saturating_add(1));
91	let pdus = services
92		.timeline
93		.pdus_rev(Some(sender_user), room_id, until);
94
95	// Take the last events for the timeline.
96	pin_mut!(pdus);
97	let mut timeline_pdus = Vec::new();
98	let mut last_timeline_count = PduCount::max();
99	let mut first = true;
100	let mut limited = false;
101
102	while let Some(pdu) = pdus.next().await {
103		let (pducount, pdu) = match pdu {
104			| Ok(pdu) => pdu,
105			| Err(error) if first || matches!(errors, TimelineErrors::Propagate) => {
106				return Err(error);
107			},
108			| Err(_) => continue,
109		};
110
111		if first {
112			first = false;
113			last_timeline_count = matches!(pducount, PduCount::Normal(_))
114				.then_some(pducount)
115				.unwrap_or_else(PduCount::max);
116		}
117
118		if pducount <= roomsincecount {
119			break;
120		}
121
122		if timeline_pdus.len() == limit {
123			limited = true;
124			break;
125		}
126
127		timeline_pdus.push((pducount, pdu));
128	}
129
130	timeline_pdus.reverse();
131
132	Ok((timeline_pdus, limited, last_timeline_count))
133}
134
135/// Returns the backward pagination token for a timeline slice.
136///
137/// A slice beginning at room creation has nothing before it, so its window's
138/// end lets a members query describe the room as received.
139/// Backward pagination from that token re-reads the slice once before ending.
140fn timeline_prev_batch(
141	timeline_pdus: &[(PduCount, PduEvent)],
142	window_end: PduCount,
143) -> Option<PduCount> {
144	timeline_pdus
145		.first()
146		.map(|(count, pdu)| match pdu.kind() {
147			| RoomCreate => window_end,
148			| _ => *count,
149		})
150}
151
152async fn share_encrypted_room(
153	services: &Services,
154	sender_user: &UserId,
155	user_id: &UserId,
156) -> bool {
157	services
158		.state_cache
159		.get_shared_rooms(sender_user, user_id)
160		.map(ToOwned::to_owned)
161		.any(async |room_id| {
162			services
163				.state_accessor
164				.is_encrypted_room(&room_id)
165				.await
166		})
167		.await
168}
169
170/// Whether a user's join to a room is fresh since a sync token.
171///
172/// The joined count moves on every join and forced state replay except a local
173/// user's profile change over the position its join recorded, which keeps it.
174/// An unreadable count reads as unmoved.
175#[tracing::instrument(level = "trace", skip(services))]
176async fn joined_since(
177	services: &Services,
178	room_id: &RoomId,
179	user_id: &UserId,
180	since: u64,
181) -> bool {
182	services
183		.state_cache
184		.get_joined_count(room_id, user_id)
185		.await
186		.inspect_err(|error| debug_warn!(%room_id, %user_id, %error, "joined count unreadable"))
187		.is_ok_and(|recorded| recorded > since)
188}
189
190/// MSC4155: whether a stored invite may be served to the invitee.
191///
192/// The verdict is the recipient's own, judged against the sender of the
193/// stripped invite membership event. Unreadable invite state takes the same
194/// verdict as the sender-less case below, since neither can name a sender.
195async fn invite_permitted_room(
196	services: &Services,
197	user_id: &UserId,
198	room_id: &RoomId,
199	filter: &InviteFilter,
200) -> bool {
201	filter.is_permissive()
202		|| services
203			.state_cache
204			.invite_state(user_id, room_id)
205			.await
206			.map_or_else(
207				|error| {
208					debug_warn!(%user_id, %room_id, ?error, "invite state is unreadable; skipping the sender rules");
209					filter.permits(None)
210				},
211				|invite_state| invite_permitted(user_id, room_id, filter, &invite_state),
212			)
213}
214
215/// [`invite_permitted_room`] for a room whose stripped state is in hand.
216///
217/// Callers walking stored invites already hold the state and take this form,
218/// which spares them the load the room-keyed form pays per room. `room_id`
219/// names the affected row in the sender-less diagnostic and does not enter the
220/// verdict. That diagnostic stays at debug on a release build deliberately,
221/// since the condition repeats for every sync an affected user makes.
222fn invite_permitted(
223	user_id: &UserId,
224	room_id: &RoomId,
225	filter: &InviteFilter,
226	invite_state: &[Raw<AnyStrippedStateEvent>],
227) -> bool {
228	// Load-bearing: keeps a permissive user off the sender derivation below.
229	if filter.is_permissive() {
230		return true;
231	}
232
233	let sender = invite_sender(user_id, invite_state);
234
235	if sender.is_none() {
236		debug_warn!(%user_id, %room_id, "invite state names no sender; skipping the sender rules");
237	}
238
239	filter.permits(sender.as_deref())
240}
241
242/// The sender of the stripped membership event inviting `user_id`.
243///
244/// The last matching entry wins. An invite this server recorded after the
245/// federation route began sanitising stripped state holds one entry for this
246/// cell, our own copy of the signed membership PDU, whose sender the origin
247/// check authenticated. An invite stored before that still carries whatever
248/// the inviting server sent ahead of our copy, and the array has no ordering
249/// semantics in the spec, so reading the last entry is what keeps those
250/// answering with the authenticated sender too.
251fn invite_sender(
252	user_id: &UserId,
253	invite_state: &[Raw<AnyStrippedStateEvent>],
254) -> Option<OwnedUserId> {
255	invite_state
256		.iter()
257		.rev()
258		.filter(|event| {
259			event
260				.get_field::<&str>("state_key")
261				.is_ok_and(|state_key| state_key.is_some_and(is_equal_to!(user_id.as_str())))
262		})
263		.filter_map(|event| event.deserialize().ok())
264		.find_map(|event| match event {
265			| AnyStrippedStateEvent::RoomMember(member) if member.state_key == user_id =>
266				Some(member.sender),
267			| _ => None,
268		})
269}
270
271/// Strips the unsigned fields a state section does not serve.
272///
273/// Fields private to the sender go unless the requester sent the event. The
274/// stored `prev_content`/`prev_sender` pair goes too (Synapse injects it on
275/// timeline fetches only), except on the requester's own membership and on
276/// events duplicated from the returned timeline (MSC4222, full_state), the
277/// copies clients read membership transitions from.
278fn strip_state_unsigned(
279	mut pdu: PduEvent,
280	sender_user: &UserId,
281	in_timeline: impl Fn(&PduEvent) -> bool,
282) -> PduEvent {
283	let own_membership =
284		*pdu.kind() == RoomMember && pdu.state_key() == Some(sender_user.as_str());
285
286	pdu.remove_private_unless_sender(Some(sender_user));
287
288	if !own_membership && !in_timeline(&pdu) {
289		pdu.remove_prev_state().log_err().ok();
290	}
291
292	pdu
293}
294
295/// The user's own departure in a left room's state, when it holds one.
296///
297/// A ban counts, since the membership indexes file a banned user as left. Any
298/// other membership yields `None`, as a room this server left still holds the
299/// invite a later rescission withdrew. A failed read is logged and yields `None`
300/// too, so the client still learns of the departure. A kick or ban is sent by
301/// someone else, so fields private to the sender are removed unless the user
302/// sent it.
303async fn own_departure(
304	services: &Services,
305	room_id: &RoomId,
306	user_id: &UserId,
307) -> Option<PduEvent> {
308	services
309		.state_accessor
310		.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())
311		.await
312		.optional()
313		.inspect_err(|error| warn!(%room_id, %user_id, %error, "Unreadable own departure event"))
314		.ok()
315		.flatten()
316		.filter(|event| {
317			matches!(
318				event.membership_for(user_id),
319				Some(MembershipState::Leave | MembershipState::Ban)
320			)
321		})
322		.map(|event| event.without_private_unless_sender(Some(user_id)))
323}
324
325/// Builds a minimal leave event for a left room whose state supplies none.
326///
327/// A rescinded or rejected federated invite, or deleted, missing, or broken
328/// room state, leaves no real membership event to send, while clients read the
329/// departure from one. The stand-in names the user as sender under a fresh
330/// local event ID.
331fn stand_in_leave(
332	services: &Services,
333	room_id: &RoomId,
334	sender_user: &UserId,
335) -> Result<Raw<AnySyncStateEvent>> {
336	let event = PduEvent {
337		event_id: EventId::new_v1(services.globals.server_name()),
338		origin_server_ts: now_millis().try_into()?,
339		kind: RoomMember,
340		state_key: Some(sender_user.as_str().into()),
341		sender: sender_user.to_owned(),
342		content: serde_json::from_str(r#"{"membership":"leave"}"#)?,
343		// The following keys are dropped on conversion
344		room_id: room_id.to_owned(),
345		depth: uint!(1),
346		origin: None,
347		unsigned: None,
348		redacts: None,
349		hashes: EventHash::default(),
350		auth_events: Default::default(),
351		prev_events: Default::default(),
352	};
353
354	Ok(event.into_format())
355}