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
98const 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#[derive(Clone, Copy, Debug, Default)]
122struct Candidate {
123 count: Option<PduCount>,
125
126 lazy: bool,
128
129 left_unread: bool,
131
132 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 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 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 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() .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 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
426fn 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
463fn 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
577fn 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 let state_unchanged = !changed
629 || since_state
630 .is_some_and(|(_, since_shortstatehash)| current == Some(since_shortstatehash));
631
632 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 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 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 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
775async 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 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
815fn 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
843fn 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
861async 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}