Skip to main content

tuwunel_api_client/sync/v5/
rooms.rs

1mod bump_stamp;
2mod collected;
3mod heroes;
4
5#[cfg(test)]
6mod tests;
7
8use std::{
9	borrow::Cow,
10	collections::{BTreeMap, HashSet},
11};
12
13use futures::{
14	FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt,
15	future::{join, join3, join4},
16};
17use ruma::{
18	JsOption, MxcUri, OwnedEventId, OwnedMxcUri, RoomId, UInt, UserId,
19	api::client::sync::sync_events::{
20		UnreadNotificationsCount,
21		v5::{
22			DisplayName,
23			response::{Heroes, Room as ResponseRoom},
24		},
25	},
26	events::{
27		AnyStrippedStateEvent, AnySyncStateEvent, StateEventType, TimelineEventType,
28		room::member::MembershipState,
29	},
30	serde::Raw,
31};
32use serde::Deserialize;
33use tuwunel_core::{
34	Error, Result, at, format_small_string, is_equal_to, ref_at,
35	smallstr::SmallString,
36	smallvec::SmallVec,
37	utils::{
38		BoolExt, IterStream, OptionExt, ReadyExt, TryFutureExtExt,
39		hash::sha256::{
40			Digest as Sha256Digest, delimited as sha256_delimited, hash as sha256_hash,
41		},
42		math::usize_from_ruma_bounded,
43		result::{FlatOk, NotFound},
44		stream::{BroadbandExt, WidebandExt},
45	},
46	warn,
47};
48use tuwunel_matrix::{
49	Event, StateKey,
50	pdu::{PduCount, PduEvent, RawPduId},
51};
52use tuwunel_service::{
53	Services,
54	rooms::short::ShortStateHash,
55	sync::{RequiredState, Room, RoomConfig, UnreadState},
56};
57
58use self::{
59	bump_stamp::{knock_bump_stamp, room_bump_stamp},
60	collected::{Collected, Outcome, Unread},
61	heroes::calculate_heroes,
62};
63use super::{
64	super::{load_timeline_fallible, own_departure, stand_in_leave, strip_state_unsigned},
65	Connection, ListIds, SyncInfo, WindowRoom, departed, stripped_content, stripped_state,
66};
67use crate::{ignored_filter, with_membership};
68
69#[derive(Debug)]
70pub(super) enum Failure {
71	Timeline(Error),
72	Payload(Error),
73}
74
75#[derive(Deserialize)]
76struct NameContent<'a> {
77	#[serde(borrow)]
78	name: Cow<'a, str>,
79}
80
81#[derive(Deserialize)]
82struct AvatarContent {
83	url: Option<OwnedMxcUri>,
84}
85
86type ThreadCounts = BTreeMap<OwnedEventId, (u64, u64)>;
87type EventTypeString = SmallString<[u8; 32]>;
88type TimelineMembers<'a> = SmallVec<[&'a str; 2]>;
89pub(super) type RoomDetails = (usize, HashSet<(StateEventType, StateKey)>);
90pub(super) type Delivery = (ResponseRoom, StateRead);
91type StateRead = (Option<ShortStateHash>, UnreadState);
92type StateEvent = Raw<AnySyncStateEvent>;
93type StrippedEvent = Raw<AnyStrippedStateEvent>;
94type Selected = ((StateEventType, StateKey), Option<OwnedEventId>, bool);
95type CollectedState = (Vec<StateEvent>, StateRead);
96type Snapshots = (Option<ShortStateHash>, Option<(PduCount, ShortStateHash)>);
97
98/// Most timeline events one room returns per response, as legacy sync allows.
99const TIMELINE_LIMIT_MAX: usize = 100;
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub(super) enum StateMode {
103	Full,
104	Delta(PduCount),
105}
106
107#[derive(Clone, Copy)]
108struct StateSelection<'a> {
109	mode: StateMode,
110	previous: Option<&'a [u64]>,
111	changed: bool,
112	delivered: ShortStateHash,
113	unread: &'a [u64],
114}
115
116/// What decides whether a delta sends one requested state event.
117///
118/// The event is sent when it is newer than the delta's base, when the room held
119/// a different event at the base, when the last delivery could not read it, or
120/// when it is a lazily loaded member.
121#[derive(Clone, Copy, Debug, Default)]
122struct Candidate {
123	/// The event's timeline position, absent for an outlier.
124	count: Option<PduCount>,
125
126	/// Whether it was selected as a lazily loaded member.
127	lazy: bool,
128
129	/// Whether the last delivery found its event due but could not read it.
130	left_unread: bool,
131
132	/// Whether the room held the same event at the delta's base.
133	same_at_since: bool,
134}
135
136#[tracing::instrument(
137	name = "room",
138	level = "debug",
139	skip_all,
140	fields(room_id, roomsince)
141)]
142pub(super) async fn handle_room(
143	sync_info: SyncInfo<'_>,
144	conn: &Connection,
145	window_room: &WindowRoom,
146	room: &Room,
147	config_changed: bool,
148	room_details: RoomDetails,
149) -> Result<Delivery, Failure> {
150	let SyncInfo {
151		services,
152		sender_user,
153		previous_connection_pos,
154		direct_rooms,
155		..
156	} = sync_info;
157
158	let WindowRoom { lists, membership, room_id, .. } = window_room;
159	let roomsince = room.roomsince;
160
161	if departed(membership.as_ref()) {
162		return leave_or_ban_response(sync_info, conn, window_room, roomsince)
163			.map_ok(|payload| (payload, (None, room.unread_state.clone())))
164			.map_err(Failure::Payload)
165			.await;
166	}
167
168	let pending = matches!(membership, Some(MembershipState::Invite | MembershipState::Knock));
169
170	let encrypted = services.state_accessor.is_encrypted_room(room_id);
171
172	let (timeline_limit, required_state) = room_details;
173	let allowed = membership_allows_required_state(membership.as_ref());
174
175	let timeline = allowed.then_async(|| {
176		load_timeline_fallible(
177			services,
178			sender_user,
179			room_id,
180			PduCount::Normal(roomsince),
181			Some(PduCount::from(conn.next_batch)),
182			timeline_limit,
183		)
184	});
185
186	let timeline = timeline
187		.map(Option::transpose)
188		.map_err(Failure::Timeline);
189
190	let (encrypted, timeline) = join(encrypted, timeline).await;
191
192	// A failed load must fail the room, else roomsince advances past unsent events.
193	let (timeline_pdus, limited, last_timeline_count) =
194		timeline?.unwrap_or_else(|| (Vec::new(), false, PduCount::default()));
195
196	let limited = room_timeline_limited(timeline_limit, limited);
197
198	let prev_batch = timeline_pdus
199		.first()
200		.map(at!(0))
201		.map(PduCount::into_unsigned)
202		.as_ref()
203		.map(ToString::to_string);
204
205	let bump_stamp = match membership {
206		| Some(MembershipState::Knock) => {
207			// Redelivered rooms retain their own membership baseline even before roomsince.
208			knock_bump_stamp(window_room.event_count, conn.next_batch)
209		},
210		| _ =>
211			room_bump_stamp(
212				services,
213				sender_user,
214				room_id,
215				PduCount::Normal(roomsince),
216				PduCount::from(conn.next_batch),
217				last_timeline_count,
218			)
219			.map_err(Failure::Timeline)
220			.await?,
221	};
222
223	let mode = state_mode(roomsince, room.required_state.is_empty());
224	let state = StateSelection {
225		mode,
226		previous: config_changed.then_some(room.required_state.as_slice()),
227		changed: allowed && state_may_have_changed(mode, last_timeline_count),
228		delivered: room.shortstatehash,
229		unread: &room.unread_state,
230	};
231
232	let required_state = allowed
233		.and_is(state.changed || state.previous.is_some())
234		.then_some(required_state)
235		.unwrap_or_default();
236
237	let required_state = collect_required_state(
238		services,
239		sender_user,
240		room_id,
241		state,
242		&required_state,
243		&timeline_pdus,
244		encrypted,
245	);
246
247	// TODO: figure out a timestamp we can use for remote invites
248	let stripped_state = stripped_state(services, sender_user, room_id, membership.as_ref());
249
250	let timeline = timeline_pdus
251		.iter()
252		.stream()
253		.filter_map(|item| ignored_filter(services, item.clone(), sender_user))
254		.wide_then(|(position, pdu)| {
255			with_membership(services, pdu, sender_user, encrypted).map(move |pdu| (position, pdu))
256		})
257		.wide_then(|(position, pdu)| {
258			services
259				.pdu_metadata
260				.bundle_aggregations(sender_user, pdu)
261				.map(move |pdu| (position, pdu))
262		})
263		.map(|(position, pdu)| (position, Event::into_format(pdu)))
264		.collect::<Vec<_>>();
265
266	let meta = room_meta_future(services, room_id);
267	let events = join3(timeline, required_state, stripped_state);
268	let member_counts = member_counts_future(services, room_id);
269	let notification_counts = notification_counts_future(services, sender_user, room_id);
270	let (
271		(room_name, room_avatar),
272		(timeline, (required_state, state_read), stripped_state),
273		(joined_count, invited_count),
274		(highlight_count, notification_count, _last_notification_read, thread_counts),
275	) = join4(meta, events, member_counts, notification_counts)
276		.boxed() // size firewall
277		.await;
278
279	let (room_name, room_avatar) =
280		knock_meta(membership.as_ref(), stripped_state.as_deref(), room_name, room_avatar);
281
282	let (heroes, heroes_name, heroes_avatar) = resolve_heroes(
283		services,
284		sender_user,
285		room_id,
286		room_name.as_ref(),
287		room_avatar.as_deref(),
288	)
289	.await;
290
291	let previous_connection_pos = previous_connection_pos.filter(|_| !pending);
292	let (initial, num_live) =
293		room_timeline_metadata(roomsince, previous_connection_pos, &timeline);
294
295	let timeline = timeline.into_iter().map(at!(1)).collect();
296	let unread_notifications =
297		merge_unread_notifications(highlight_count, notification_count, &thread_counts);
298
299	let payload = ResponseRoom {
300		initial,
301		lists: lists.clone(),
302		membership: membership.clone(),
303		name: room_name.or(heroes_name),
304		avatar: JsOption::from_option(room_avatar.or(heroes_avatar)),
305		is_dm: direct_rooms.contains(room_id).then_some(true),
306		heroes,
307		required_state,
308		invite_state: stripped_state,
309		prev_batch: prev_batch.as_deref().map(Into::into),
310		num_live,
311		limited,
312		timeline,
313		bump_stamp,
314		joined_count,
315		invited_count,
316		unread_notifications,
317	};
318
319	Ok((payload, state_read))
320}
321
322async fn leave_or_ban_response(
323	SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
324	conn: &Connection,
325	WindowRoom { lists, membership, room_id, .. }: &WindowRoom,
326	roomsince: u64,
327) -> Result<ResponseRoom> {
328	// Clients read the departure from this event, not `membership`.
329	let member_event = match membership {
330		| Some(MembershipState::Ban) => services
331			.state_accessor
332			.room_state_get(room_id, &StateEventType::RoomMember, sender_user.as_str())
333			.await
334			.optional()
335			.inspect_err(|error| warn!(%room_id, %sender_user, %error, "Unreadable member event"))
336			.ok()
337			.flatten()
338			.map(|event| event.without_private_unless_sender(Some(sender_user))),
339		| _ => own_departure(services, room_id, sender_user).await,
340	};
341
342	let member_event = member_event.map(Event::into_format).or_else(|| {
343		matches!(membership, Some(MembershipState::Leave))
344			.then(|| stand_in_leave(services, room_id, sender_user))
345			.and_then(Result::ok)
346	});
347
348	Ok(ResponseRoom {
349		initial: roomsince.eq(&0).then_some(true),
350		lists: lists.clone(),
351		membership: membership.clone(),
352		prev_batch: Some(conn.next_batch.to_string().into()),
353		limited: true,
354		required_state: member_event.into_iter().collect(),
355		..Default::default()
356	})
357}
358
359pub(super) fn merged_room_details(
360	conn: &Connection,
361	lists: &ListIds,
362	room_id: &RoomId,
363) -> RoomDetails {
364	lists
365		.iter()
366		.filter_map(|list_id| conn.lists.get(list_id))
367		.map(|list| &list.room_details)
368		.chain(conn.subscriptions.get(room_id))
369		.fold((0_usize, HashSet::new()), |mut details, config| {
370			details.0 = details.0.max(usize_from_ruma_bounded(
371				config.timeline_limit,
372				TIMELINE_LIMIT_MAX,
373				TIMELINE_LIMIT_MAX,
374			));
375
376			details
377				.1
378				.extend(config.required_state.iter().cloned());
379
380			details
381		})
382}
383
384pub(super) fn room_config((timeline_limit, required_state): &RoomDetails) -> RoomConfig {
385	let timeline_limit = u64::try_from(*timeline_limit).expect("timeline limit must fit u64");
386	let digest = sha256_hash(timeline_limit.to_be_bytes());
387
388	required_state.iter().fold(
389		(digest_word(digest), RequiredState::new()),
390		|(hash, mut selectors), (event_type, state_key)| {
391			let entry = required_state_hash(event_type, state_key.as_str());
392
393			selectors.extend(state_key.as_str().ne("$LAZY").then_some(entry));
394
395			(hash ^ entry, selectors)
396		},
397	)
398}
399
400fn state_mode(roomsince: u64, unknown: bool) -> StateMode {
401	match (roomsince, unknown) {
402		| (0, _) | (_, true) => StateMode::Full,
403		| (roomsince, false) => StateMode::Delta(PduCount::Normal(roomsince)),
404	}
405}
406
407fn required_state_hash(event_type: &StateEventType, state_key: &str) -> u64 {
408	let event_type: EventTypeString = format_small_string!("{event_type}");
409	let digest = sha256_delimited([event_type.as_str(), state_key].into_iter());
410
411	digest_word(digest)
412}
413
414fn digest_word(digest: Sha256Digest) -> u64 {
415	u64::from_be_bytes(
416		digest[..8]
417			.try_into()
418			.expect("SHA-256 digest must contain eight bytes"),
419	)
420}
421
422pub(super) fn membership_allows_required_state(membership: Option<&MembershipState>) -> bool {
423	matches!(membership, None | Some(MembershipState::Join))
424}
425
426/// Whether a room's newest event lies past the delta cursor.
427///
428/// State changes arrive as timeline events, so a room with nothing newer than
429/// the cursor has none to report. A full sync always reports.
430fn state_may_have_changed(state_mode: StateMode, last_timeline_count: PduCount) -> bool {
431	match state_mode {
432		| StateMode::Full => true,
433		| StateMode::Delta(since) => last_timeline_count > since,
434	}
435}
436
437fn room_timeline_limited(timeline_limit: usize, limited: bool) -> bool {
438	timeline_limit > 0 && limited
439}
440
441fn room_timeline_metadata<Event>(
442	roomsince: u64,
443	previous_connection_pos: Option<u64>,
444	timeline_pdus: &[(PduCount, Event)],
445) -> (Option<bool>, Option<UInt>) {
446	let initial = roomsince.eq(&0).then_some(true);
447	let num_live = previous_connection_pos
448		.map(PduCount::from)
449		.and_then(|previous_connection_pos| {
450			timeline_pdus
451				.iter()
452				.rev()
453				.map(|(position, _)| *position)
454				.take_while(|position| *position > previous_connection_pos)
455				.count()
456				.try_into()
457				.ok()
458		});
459
460	(initial, num_live)
461}
462
463/// The name and avatar a knocked room's summary gives where room state has none.
464///
465/// A remote knock leaves this server no room state, so its summary names the
466/// room.
467fn knock_meta(
468	membership: Option<&MembershipState>,
469	stripped_state: Option<&[StrippedEvent]>,
470	room_name: Option<DisplayName>,
471	room_avatar: Option<OwnedMxcUri>,
472) -> (Option<DisplayName>, Option<OwnedMxcUri>) {
473	let summary = stripped_state
474		.filter(|_| membership == Some(&MembershipState::Knock))
475		.unwrap_or_default();
476
477	let room_name = room_name.or_else(|| {
478		stripped_content(summary, &StateEventType::RoomName)
479			.filter(|content: &NameContent<'_>| !content.name.is_empty())
480			.map(|content| DisplayName::from(&*content.name))
481	});
482
483	let room_avatar = room_avatar.or_else(|| {
484		stripped_content(summary, &StateEventType::RoomAvatar)
485			.and_then(|content: AvatarContent| content.url)
486	});
487
488	(room_name, room_avatar)
489}
490
491async fn resolve_heroes(
492	services: &Services,
493	sender_user: &UserId,
494	room_id: &RoomId,
495	room_name: Option<&DisplayName>,
496	room_avatar: Option<&MxcUri>,
497) -> (Option<Heroes>, Option<DisplayName>, Option<OwnedMxcUri>) {
498	services
499		.config
500		.client
501		.sync
502		.calculate_heroes
503		.then_async(|| calculate_heroes(services, sender_user, room_id, room_name, room_avatar))
504		.await
505		.unwrap_or_default()
506}
507
508fn room_meta_future<'a>(
509	services: &'a Services,
510	room_id: &'a RoomId,
511) -> impl Future<Output = (Option<DisplayName>, Option<OwnedMxcUri>)> + Send + 'a {
512	let room_name = services
513		.state_accessor
514		.get_name(room_id)
515		.map_ok(Into::into)
516		.map(Result::ok);
517
518	let room_avatar = services
519		.state_accessor
520		.get_avatar(room_id)
521		.map_ok(|content| content.url)
522		.ok()
523		.map(Option::flatten);
524
525	join(room_name, room_avatar)
526}
527
528fn member_counts_future<'a>(
529	services: &'a Services,
530	room_id: &'a RoomId,
531) -> impl Future<Output = (Option<UInt>, Option<UInt>)> + Send + 'a {
532	let joined_count = services
533		.state_cache
534		.room_joined_count(room_id)
535		.map_ok(TryInto::try_into)
536		.map_ok(Result::ok)
537		.map(FlatOk::flat_ok);
538
539	let invited_count = services
540		.state_cache
541		.room_invited_count(room_id)
542		.map_ok(TryInto::try_into)
543		.map_ok(Result::ok)
544		.map(FlatOk::flat_ok);
545
546	join(joined_count, invited_count)
547}
548
549fn notification_counts_future<'a>(
550	services: &'a Services,
551	sender_user: &'a UserId,
552	room_id: &'a RoomId,
553) -> impl Future<Output = (Option<UInt>, Option<UInt>, Result<u64>, ThreadCounts)> + Send + 'a {
554	let highlight_count = services
555		.pusher
556		.highlight_count(sender_user, room_id)
557		.map(TryInto::try_into)
558		.map(Result::ok);
559
560	let notification_count = services
561		.pusher
562		.notification_count(sender_user, room_id)
563		.map(TryInto::try_into)
564		.map(Result::ok);
565
566	let last_read_count = services
567		.pusher
568		.last_notification_read(sender_user, room_id);
569
570	let thread_counts = services
571		.pusher
572		.thread_notification_counts(sender_user, room_id);
573
574	join4(highlight_count, notification_count, last_read_count, thread_counts)
575}
576
577// MSC3771/MSC3773: SSS v5 has no per-thread bucket; fold into the room total.
578fn merge_unread_notifications(
579	highlight_count: Option<UInt>,
580	notification_count: Option<UInt>,
581	thread_counts: &ThreadCounts,
582) -> UnreadNotificationsCount {
583	let (thread_notifications, thread_highlights) = thread_counts
584		.values()
585		.fold((0_u64, 0_u64), |(n, h), &(notifs, hl)| {
586			(n.saturating_add(notifs), h.saturating_add(hl))
587		});
588
589	let merge = |total: u64| {
590		move |count: UInt| count.saturating_add(UInt::try_from(total).unwrap_or_default())
591	};
592
593	UnreadNotificationsCount {
594		highlight_count: highlight_count.map(merge(thread_highlights)),
595		notification_count: notification_count.map(merge(thread_notifications)),
596	}
597}
598
599async fn collect_required_state(
600	services: &Services,
601	sender_user: &UserId,
602	room_id: &RoomId,
603	selection: StateSelection<'_>,
604	required_state: &HashSet<(StateEventType, StateKey)>,
605	timeline_pdus: &[(PduCount, PduEvent)],
606	encrypted: bool,
607) -> CollectedState {
608	let StateSelection {
609		mode: state_mode,
610		previous,
611		changed,
612		delivered,
613		unread: unread_keys,
614	} = selection;
615
616	let lazy = required_state
617		.iter()
618		.any(is_equal_to!(&(StateEventType::RoomMember, "$LAZY".into())));
619
620	let compared = changed
621		&& required_state
622			.iter()
623			.any(|(_, state_key)| state_key != "$LAZY");
624
625	let (current, since_state) = state_snapshots(services, room_id, selection, compared).await;
626
627	// Equal hashes exclude changes, but newly requested keys may still be due.
628	let state_unchanged = !changed
629		|| since_state
630			.is_some_and(|(_, since_shortstatehash)| current == Some(since_shortstatehash));
631
632	// Keys skipped as unchanged vouch for no snapshot, so the room keeps its last one.
633	let snapshot = current.filter(|_| !state_unchanged);
634
635	let timeline_senders = timeline_pdus
636		.iter()
637		.filter(|_| lazy)
638		.map(ref_at!(1))
639		.map(Event::sender)
640		.map(UserId::as_str);
641
642	let timeline_member_targets = timeline_pdus
643		.iter()
644		.filter(|_| lazy)
645		.map(ref_at!(1))
646		.filter(|event| *event.event_type() == TimelineEventType::RoomMember)
647		.filter_map(Event::state_key);
648
649	let mut timeline_members: TimelineMembers<'_> = timeline_senders
650		.chain(timeline_member_targets)
651		.collect();
652
653	timeline_members.sort_unstable();
654	timeline_members.dedup();
655
656	let timeline_members = timeline_members
657		.into_iter()
658		.map(|sender| (StateEventType::RoomMember, StateKey::from_str(sender)));
659
660	let in_timeline = |event: &PduEvent| {
661		timeline_pdus
662			.iter()
663			.map(ref_at!(1))
664			.map(Event::event_id)
665			.any(is_equal_to!(event.event_id()))
666	};
667
668	let evaluated = !state_unchanged || previous.is_some();
669
670	required_state
671		.iter()
672		.filter(|_| evaluated)
673		.cloned()
674		.map(|state| Ok((state, None, false)))
675		.stream()
676		.chain(wildcard_state(services, room_id, required_state, evaluated))
677		.chain(
678			timeline_members
679				.map(|state| Ok((state, None, true)))
680				.stream(),
681		)
682		.broad_then(async |selected| -> Outcome {
683			let ((event_type, state_key), event_id, lazy) = selected?;
684			let state_key: StateKey = match state_key.as_str() {
685				| "$LAZY" | "*" => return Ok(None),
686				| "$ME" => sender_user.as_str().into(),
687				| _ => state_key,
688			};
689
690			let state_mode = previous
691				.filter(|previous| {
692					!fingerprints_cover(previous, &event_type, state_key.as_str(), sender_user)
693				})
694				.map(|_| StateMode::Full)
695				.unwrap_or(state_mode);
696
697			if state_unchanged && !lazy && state_mode != StateMode::Full {
698				return Ok(None);
699			}
700
701			// A key that cannot be resolved is not yet known to be due, so it records zero.
702			let event_id = match event_id {
703				| Some(event_id) => Some(event_id),
704				| None =>
705					current_state_id(services, room_id, &event_type, &state_key)
706						.map_err(|_| Unread::Unresolved)
707						.await?,
708			};
709
710			let Some(event_id) = event_id else {
711				return Ok(None);
712			};
713
714			let pdu_id = services.timeline.get_pdu_id(&event_id).await.ok();
715			let count = pdu_id.map(RawPduId::pdu_count);
716			let left_unread =
717				fingerprints_cover(unread_keys, &event_type, state_key.as_str(), sender_user);
718
719			let comparable = !lazy && !left_unread && state_mode != StateMode::Full;
720
721			// Only a delivered snapshot vouches for an outlier; the cursor's snapshot
722			// may already hold one the client was never sent.
723			let same_at_since = since_state
724				.filter(|(since, _)| {
725					comparable && count.map_or(delivered != 0, |count| count <= *since)
726				})
727				.map_async(|(_, shortstatehash)| {
728					services
729						.state_accessor
730						.state_get_id(shortstatehash, &event_type, &state_key)
731						.ok()
732				})
733				.await
734				.flatten()
735				.is_some_and(|previous_event_id| previous_event_id == event_id);
736
737			let candidate = Candidate { count, lazy, left_unread, same_at_since };
738
739			if !state_is_required(state_mode, candidate) {
740				return Ok(None);
741			}
742
743			let pdu = match pdu_id {
744				| None => services.timeline.get_outlier_pdu(&event_id).await,
745				| Some(pdu_id) =>
746					services
747						.timeline
748						.get_pdu_from_id(&pdu_id)
749						.or_else(|_| services.timeline.get_outlier_pdu(&event_id))
750						.await,
751			};
752
753			// A due event that cannot be read leaves its key for the next delivery to send.
754			let unread_key =
755				|_: Error| Unread::Unreadable(required_state_hash(&event_type, &state_key));
756
757			let pdu = pdu
758				.inspect_err(|error| {
759					warn!(%room_id, %event_id, %error, "Unreadable required state event");
760				})
761				.map_err(unread_key)?;
762
763			let pdu = with_membership(services, pdu, sender_user, encrypted).await;
764			let pdu = strip_state_unsigned(pdu, sender_user, in_timeline);
765
766			let settled = left_unread.then(|| required_state_hash(&event_type, &state_key));
767
768			Ok(Some((Event::into_format(pdu), settled)))
769		})
770		.ready_fold_default(Collected::push)
771		.map(|collected| collected.finish(snapshot, unread_keys))
772		.await
773}
774
775/// The room's current snapshot and the base a delta compares requested state with.
776///
777/// The current snapshot is read before any state key, so a change racing the
778/// read is sent again rather than lost. The base is the snapshot the room's
779/// required state was last delivered from, or without one, the snapshot before
780/// the first event past the cursor.
781async fn state_snapshots(
782	services: &Services,
783	room_id: &RoomId,
784	StateSelection { mode, changed, delivered, .. }: StateSelection<'_>,
785	compared: bool,
786) -> Snapshots {
787	let current = changed
788		.then_async(|| {
789			services
790				.state
791				.get_room_shortstatehash(room_id)
792				.inspect_err(|error| warn!(%room_id, %error, "Unreadable room state snapshot"))
793				.ok()
794		})
795		.await
796		.flatten();
797
798	// Falling back to current state would match every entry against itself.
799	let since_state = match mode {
800		| StateMode::Delta(since) if compared && delivered != 0 => Some((since, delivered)),
801		| StateMode::Delta(since) if compared =>
802			services
803				.timeline
804				.next_shortstatehash(room_id, since)
805				.map_ok(|shortstatehash| (since, shortstatehash))
806				.inspect_err(|error| warn!(%room_id, %error, "Unreadable state past the cursor"))
807				.ok()
808				.await,
809		| _ => None,
810	};
811
812	(current, since_state)
813}
814
815/// The current state listed under each wildcard selector a delivery evaluates.
816///
817/// A listing that cannot be read is logged and yielded as an unresolved key: it
818/// may hide an event that was due, so the delivery records zero.
819fn wildcard_state<'a>(
820	services: &'a Services,
821	room_id: &'a RoomId,
822	required_state: &'a HashSet<(StateEventType, StateKey)>,
823	evaluated: bool,
824) -> impl Stream<Item = Result<Selected, Unread>> + Send + 'a {
825	required_state
826		.iter()
827		.filter(move |(_, state_key)| evaluated && state_key == "*")
828		.stream()
829		.flat_map(move |(event_type, _)| {
830			services
831				.state_accessor
832				.room_state_keys_with_ids(room_id, event_type)
833				.inspect_err(move |error| {
834					warn!(%room_id, %event_type, %error, "Unreadable required state keys");
835				})
836				.map_err(|_| Unread::Unresolved)
837				.map_ok(move |(state_key, event_id)| {
838					((event_type.clone(), state_key), Some(event_id), false)
839				})
840		})
841}
842
843/// Whether a set of key fingerprints covers one state key.
844///
845/// A fingerprint covers its own key, every key of its type when it is a wildcard,
846/// and the syncing user's key when it is `$ME`.
847fn fingerprints_cover(
848	fingerprints: &[u64],
849	event_type: &StateEventType,
850	state_key: &str,
851	sender_user: &UserId,
852) -> bool {
853	let contains = |key| fingerprints.contains(&required_state_hash(event_type, key));
854
855	!fingerprints.is_empty()
856		&& (contains("*")
857			|| contains(state_key)
858			|| (state_key == sender_user.as_str() && contains("$ME")))
859}
860
861/// The ID of the event the room's current state holds under one requested key.
862///
863/// A key the state does not hold is absent; any other failure is logged and
864/// returned, since the event it hides may still be due.
865async fn current_state_id(
866	services: &Services,
867	room_id: &RoomId,
868	event_type: &StateEventType,
869	state_key: &str,
870) -> Result<Option<OwnedEventId>> {
871	services
872		.state_accessor
873		.room_state_get_id(room_id, event_type, state_key)
874		.map(NotFound::present)
875		.inspect_err(|error| {
876			warn!(%room_id, %event_type, %state_key, %error, "Unreadable required state key");
877		})
878		.await
879}
880
881fn state_is_required(
882	state_mode: StateMode,
883	Candidate { count, lazy, left_unread, same_at_since }: Candidate,
884) -> bool {
885	lazy || left_unread
886		|| match state_mode {
887			| StateMode::Full => true,
888			| StateMode::Delta(since) =>
889				count.is_some_and(|count| count > since) || !same_at_since,
890		}
891}