1use std::collections::{BTreeMap, BTreeSet, btree_map::Entry};
2
3use futures::{Stream, StreamExt, TryStreamExt, future::ready, stream::once};
4use itertools::{Either, Itertools};
5use ruma::{
6 OwnedRoomId, OwnedUserId, RoomId, UserId,
7 api::client::{
8 filter::{FilterDefinition, LazyLoadOptions},
9 sync::sync_events::v3::{JoinedRoom, Rooms, State, UserUpdate},
10 },
11 events::{AnySyncStateEvent, StateEventType, room::member::MembershipState},
12 profile::ProfileFieldName,
13 serde::Raw,
14};
15use serde::Deserialize;
16use serde_json::Value;
17use tuwunel_core::{
18 Result,
19 utils::{
20 BoolExt, IterStream, TryReadyExt,
21 result::NotFound,
22 stream::{BroadbandExt, TryBroadbandExt},
23 },
24 warn,
25};
26use tuwunel_service::{Services, profile::ProfileChange};
27
28pub(super) type Fields = BTreeSet<ProfileFieldName>;
34
35pub(super) type Changes = BTreeMap<OwnedUserId, Fields>;
40
41#[derive(Default)]
46struct Candidates(BTreeMap<OwnedUserId, Candidate>);
47
48type Delta = (OwnedUserId, ProfileFieldName);
49
50enum Input {
51 Collected(Candidates),
52 Base(OwnedUserId),
53 Delta(Delta),
54}
55
56struct Candidate(Fields);
61
62pub(super) type FieldValue = (ProfileFieldName, Result<Option<Value>>);
67
68type Updates = BTreeMap<ProfileFieldName, Value>;
73
74type Users = BTreeMap<OwnedUserId, UserUpdate>;
79
80#[derive(Deserialize)]
86struct MemberContent {
87 membership: MembershipState,
88}
89
90#[tracing::instrument(name = "profiles", level = "trace", skip_all)]
97pub(super) async fn collect(
98 services: &Services,
99 sender_user: &UserId,
100 since: Option<u64>,
101 next_batch: u64,
102 filter: &FilterDefinition,
103 rooms: &Rooms,
104) -> Result<Users> {
105 let requested = filter.profile_fields.ids.as_slice();
106
107 if requested.is_empty() {
108 return Ok(Users::new());
109 }
110
111 let changes =
112 changed(services, sender_user, since.unwrap_or(0), next_batch, requested).await?;
113
114 let changes =
115 witnessed(services, sender_user, since.is_none(), rooms, filter, changes).await?;
116
117 let requested: Vec<_> = changes
118 .0
119 .values()
120 .any(is_base)
121 .then(|| {
122 requested
123 .iter()
124 .cloned()
125 .sorted_unstable()
126 .dedup()
127 .collect()
128 })
129 .unwrap_or_default();
130
131 changes
132 .0
133 .into_iter()
134 .stream()
135 .broad_then(|(user_id, fields)| {
136 collect_user(services, sender_user, user_id, fields, &requested)
137 })
138 .ready_try_filter_map(Result::Ok)
139 .ready_try_filter(|(_, update)| carries_a_field(update))
140 .try_collect()
141 .await
142}
143
144#[tracing::instrument(level = "trace", skip_all)]
152async fn changed(
153 services: &Services,
154 sender_user: &UserId,
155 since: u64,
156 next_batch: u64,
157 requested: &[ProfileFieldName],
158) -> Result<Candidates> {
159 let rooms: Vec<OwnedRoomId> = services
160 .state_cache
161 .rooms_joined_checked(sender_user)
162 .map_ok(ToOwned::to_owned)
163 .try_collect()
164 .await?;
165
166 let peers = room_changes(services, &rooms, since, next_batch, requested);
167
168 services
169 .profile
170 .try_profile_changed(sender_user, since, Some(next_batch))
171 .ready_try_filter(move |(_, field)| was_requested(requested, field))
172 .map_ok(own_change)
173 .chain(peers)
174 .try_collect()
175 .await
176}
177
178#[tracing::instrument(level = "trace", skip_all)]
179fn room_changes<'a>(
180 services: &'a Services,
181 rooms: &'a [OwnedRoomId],
182 since: u64,
183 next_batch: u64,
184 requested: &'a [ProfileFieldName],
185) -> impl Stream<Item = Result<Input>> + Send + 'a {
186 rooms
187 .iter()
188 .map(move |room_id: &OwnedRoomId| {
189 room_changed(services, room_id, since, next_batch, requested)
190 })
191 .stream()
192 .flatten()
193}
194
195#[tracing::instrument(level = "trace", skip_all)]
196fn room_changed<'a>(
197 services: &'a Services,
198 room_id: &'a RoomId,
199 since: u64,
200 next_batch: u64,
201 requested: &'a [ProfileFieldName],
202) -> impl Stream<Item = Result<Input>> + Send + 'a {
203 services
204 .profile
205 .try_room_profile_changed(room_id, since, Some(next_batch))
206 .ready_try_filter(move |(_, field)| was_requested(requested, field))
207 .map_ok(own_change)
208}
209
210fn own_change((user_id, field): ProfileChange<'_>) -> Input {
211 Input::Delta((user_id.to_owned(), field.into()))
212}
213
214impl<T> FromIterator<T> for Candidates
215where
216 Self: Extend<T>,
217{
218 fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
219 let mut candidates = Self::default();
220 candidates.extend(iter);
221
222 candidates
223 }
224}
225
226impl Extend<Input> for Candidates {
227 fn extend<T: IntoIterator<Item = Input>>(&mut self, iter: T) {
228 for input in iter {
229 match input {
230 | Input::Collected(candidates) => merge_candidates(self, candidates),
231 | Input::Delta(change) => insert_delta(self, change),
232 | Input::Base(user_id) => {
233 self.0.insert(user_id, Candidate(Fields::new()));
234 },
235 }
236 }
237 }
238}
239
240fn insert_delta(candidates: &mut Candidates, (user_id, field): Delta) {
241 match candidates.0.entry(user_id) {
242 | Entry::Vacant(entry) => {
243 entry.insert(Candidate([field].into()));
244 },
245 | Entry::Occupied(entry) =>
246 if !is_base(entry.get()) {
247 entry.into_mut().0.insert(field);
248 },
249 }
250}
251
252fn merge_candidates(current: &mut Candidates, incoming: Candidates) {
253 if current.0.is_empty() {
254 current.0 = incoming.0;
255 } else {
256 for subject in incoming.0 {
257 merge_subject(current, subject);
258 }
259 }
260}
261
262fn merge_subject(candidates: &mut Candidates, (user_id, incoming): (OwnedUserId, Candidate)) {
263 match candidates.0.entry(user_id) {
264 | Entry::Occupied(entry) => merge_fields(entry.into_mut(), incoming),
265 | Entry::Vacant(entry) => {
266 entry.insert(incoming);
267 },
268 }
269}
270
271fn merge_fields(current: &mut Candidate, incoming: Candidate) {
272 match incoming {
273 | incoming if is_base(&incoming) => current.0.clear(),
274 | incoming if !is_base(current) => current.0.extend(incoming.0),
275 | _ => {},
276 }
277}
278
279fn is_base(candidate: &Candidate) -> bool { candidate.0.is_empty() }
280
281pub(super) fn fold_change(mut changes: Changes, (user_id, field): ProfileChange<'_>) -> Changes {
286 changes
287 .entry(user_id.to_owned())
288 .or_default()
289 .insert(field.into());
290
291 changes
292}
293
294#[tracing::instrument(level = "trace", skip_all)]
300async fn witnessed(
301 services: &Services,
302 sender_user: &UserId,
303 initial: bool,
304 rooms: &Rooms,
305 filter: &FilterDefinition,
306 changes: Candidates,
307) -> Result<Candidates> {
308 let own = initial.then_some(sender_user.to_owned());
309 let members = rooms
310 .join
311 .values()
312 .flat_map(joined_room_members)
313 .chain(own)
314 .map(Input::Base);
315
316 let members = once(ready(Ok(Input::Collected(changes)))).chain(members.try_stream());
317
318 if !initial {
319 return members.try_collect().await;
320 }
321
322 let rooms: Vec<OwnedRoomId> = services
323 .state_cache
324 .rooms_joined_checked(sender_user)
325 .map_ok(ToOwned::to_owned)
326 .try_collect()
327 .await?;
328
329 let initial = initial_bases(services, &rooms, filter);
330
331 members.chain(initial).try_collect().await
332}
333
334#[tracing::instrument(level = "trace", skip_all)]
335fn initial_bases<'a>(
336 services: &'a Services,
337 rooms: &'a [OwnedRoomId],
338 filter: &'a FilterDefinition,
339) -> impl Stream<Item = Result<Input>> + Send + 'a {
340 rooms
341 .iter()
342 .try_stream()
343 .broad_and_then(move |room_id: &OwnedRoomId| initial_room(services, room_id, filter))
344 .ready_try_filter_map(Result::Ok)
345 .map_ok(move |room_id: &RoomId| initial_members(services, room_id))
346 .try_flatten()
347 .map_ok(Input::Base)
348}
349
350#[tracing::instrument(level = "trace", skip_all)]
351async fn initial_room<'a>(
352 services: &Services,
353 room_id: &'a RoomId,
354 filter: &FilterDefinition,
355) -> Result<Option<&'a RoomId>> {
356 let lazy = lazy_room(services, room_id, filter)
357 .await?
358 .is_false()
359 .then_some(room_id);
360
361 Ok(lazy)
362}
363
364#[tracing::instrument(level = "trace", skip_all)]
365fn initial_members<'a>(
366 services: &'a Services,
367 room_id: &'a RoomId,
368) -> impl Stream<Item = Result<OwnedUserId>> + Send + 'a {
369 services
370 .state_cache
371 .room_members_checked(room_id)
372 .map_ok(ToOwned::to_owned)
373}
374
375#[tracing::instrument(level = "trace", skip_all)]
376async fn lazy_room(
377 services: &Services,
378 room_id: &RoomId,
379 filter: &FilterDefinition,
380) -> Result<bool> {
381 let options = [&filter.room.state.lazy_load_options, &filter.room.timeline.lazy_load_options];
382
383 if options
384 .into_iter()
385 .all(LazyLoadOptions::is_disabled)
386 {
387 return Ok(false);
388 }
389
390 let encrypted = services
391 .state_accessor
392 .room_state_get(room_id, &StateEventType::RoomEncryption, "")
393 .await
394 .optional()?;
395
396 Ok(encrypted.is_none())
397}
398
399fn joined_room_members(room: &JoinedRoom) -> impl Iterator<Item = OwnedUserId> {
405 let state = state_events(&room.state)
406 .iter()
407 .filter_map(present_member);
408
409 let timeline = room
410 .timeline
411 .events
412 .iter()
413 .filter_map(present_member);
414
415 state.chain(timeline)
416}
417
418fn state_events(state: &State) -> &[Raw<AnySyncStateEvent>] {
419 match state {
420 | State::Before(events) | State::After(events) | State::AfterUnstable(events) =>
421 events.events.as_slice(),
422 }
423}
424
425fn present_member<T>(event: &Raw<T>) -> Option<OwnedUserId> {
430 event
431 .get_field("type")
432 .ok()
433 .flatten()
434 .filter(|event_type: &StateEventType| event_type.eq(&StateEventType::RoomMember))
435 .and_then(|_| event.get_field("content").ok().flatten())
436 .filter(MemberContent::is_present)
437 .and_then(|_| event.get_field("state_key").ok().flatten())
438}
439
440impl MemberContent {
441 fn is_present(&self) -> bool {
446 matches!(self.membership, MembershipState::Join | MembershipState::Invite)
447 }
448}
449
450pub(super) fn was_requested(requested: &[ProfileFieldName], field: &str) -> bool {
455 requested
456 .iter()
457 .any(|name| name.as_str().eq(field))
458}
459
460fn carries_a_field(update: &UserUpdate) -> bool {
461 update
462 .profile_updates
463 .as_ref()
464 .is_some_and(|updates| !updates.is_empty())
465}
466
467#[tracing::instrument(level = "trace", skip_all)]
468async fn collect_user(
469 services: &Services,
470 sender_user: &UserId,
471 user_id: OwnedUserId,
472 fields: Candidate,
473 requested: &[ProfileFieldName],
474) -> Result<Option<(OwnedUserId, UserUpdate)>> {
475 if !visible(services, sender_user, &user_id).await? {
476 return Ok(None);
477 }
478
479 let fields = selected_fields(fields, requested);
480 let update = read_update(services, &user_id, fields).await?;
481
482 Ok(Some((user_id, update)))
483}
484
485fn selected_fields(
486 fields: Candidate,
487 requested: &[ProfileFieldName],
488) -> impl Iterator<Item = ProfileFieldName> + '_ {
489 if is_base(&fields) {
490 Either::Left(requested.iter().cloned())
491 } else {
492 Either::Right(fields.0.into_iter())
493 }
494}
495
496#[tracing::instrument(level = "trace", skip_all)]
497pub(super) async fn visible(
498 services: &Services,
499 sender_user: &UserId,
500 user_id: &UserId,
501) -> Result<bool> {
502 if sender_user == user_id {
503 return Ok(true);
504 }
505
506 services
507 .state_cache
508 .rooms_joined_checked(user_id)
509 .map_ok(ToOwned::to_owned)
510 .and_then(async |room_id| {
511 let joined = services
512 .state_cache
513 .get_joined_count(&room_id, sender_user)
514 .await
515 .optional()?;
516
517 Ok(joined.is_some())
518 })
519 .try_any(ready)
520 .await
521}
522
523#[tracing::instrument(level = "trace", skip_all)]
530async fn read_update(
531 services: &Services,
532 user_id: &UserId,
533 fields: impl Iterator<Item = ProfileFieldName> + Send,
534) -> Result<UserUpdate> {
535 let profile_updates: Updates = fields
536 .stream()
537 .then(|name| read_field(services, user_id, name))
538 .map(field_update)
539 .try_collect()
540 .await?;
541
542 Ok(UserUpdate::new(profile_updates))
543}
544
545pub(super) async fn read_field(
546 services: &Services,
547 user_id: &UserId,
548 name: ProfileFieldName,
549) -> FieldValue {
550 let value = services
551 .profile
552 .profile_key(user_id, &name)
553 .await
554 .optional()
555 .inspect_err(
556 |error| warn!(%user_id, %name, %error, "Failed to read a changed profile field"),
557 );
558
559 (name, value)
560}
561
562fn field_update((name, value): FieldValue) -> Result<(ProfileFieldName, Value)> {
563 value.map(|value| (name, value.unwrap_or(Value::Null)))
564}
565
566#[cfg(test)]
567mod tests {
568 use std::iter::once;
569
570 use ruma::{
571 api::client::sync::sync_events::v3::{StateEvents, Timeline},
572 user_id,
573 };
574 use serde_json::{Value, json};
575 use tuwunel_core::{Err, Result};
576
577 use super::{
578 Candidates, Input, JoinedRoom, OwnedUserId, ProfileChange, ProfileFieldName, Raw, State,
579 Updates, field_update, is_base, joined_room_members, own_change, selected_fields,
580 was_requested,
581 };
582
583 fn field(name: &str) -> ProfileFieldName { name.into() }
584
585 fn members(room: &JoinedRoom) -> Vec<OwnedUserId> { joined_room_members(room).collect() }
586
587 fn with_delta(mut candidates: Candidates, change: ProfileChange<'_>) -> Candidates {
588 candidates.extend([own_change(change)]);
589
590 candidates
591 }
592
593 fn with_base(mut candidates: Candidates, user_id: OwnedUserId) -> Candidates {
594 candidates.extend([Input::Base(user_id)]);
595
596 candidates
597 }
598
599 #[test]
600 fn candidate_deltas_union_fields_without_duplicates() {
601 let user = user_id!("@alice:example.com");
602 let changes = [(user, "org.z"), (user, "org.a"), (user, "org.z")]
603 .into_iter()
604 .map(own_change)
605 .collect::<Candidates>();
606
607 let fields = changes
608 .0
609 .into_values()
610 .next()
611 .expect("one subject");
612
613 let selected = selected_fields(fields, &[]).collect::<Vec<_>>();
614
615 assert_eq!(selected, [field("org.a"), field("org.z")]);
616 }
617
618 #[test]
619 fn vacant_delta_and_existing_base_stay_distinct() {
620 let user = user_id!("@alice:example.com");
621 let vacant = once((user, "org.z"))
622 .map(own_change)
623 .collect::<Candidates>();
624
625 let existing = once(Input::Base(user.to_owned())).collect::<Candidates>();
626
627 let existing = with_delta(existing, (user, "org.z"));
628
629 assert!(!is_base(vacant.0.values().next().expect("one delta")));
630 assert!(is_base(existing.0.values().next().expect("one base")));
631 }
632
633 #[test]
634 fn full_bases_subsume_deltas_in_either_order() {
635 let user = user_id!("@alice:example.com");
636 let requested = [field("org.a"), field("org.z")];
637
638 let delta_first = once((user, "org.z"))
639 .map(own_change)
640 .collect::<Candidates>();
641
642 let delta_first = with_base(delta_first, user.to_owned());
643 let base_first = once(Input::Base(user.to_owned())).collect::<Candidates>();
644
645 let base_first = with_delta(base_first, (user, "org.z"));
646
647 for changes in [delta_first, base_first] {
648 let fields = changes
649 .0
650 .into_values()
651 .next()
652 .expect("one subject");
653
654 assert!(is_base(&fields), "a base keeps no per-subject field set");
655
656 let selected = selected_fields(fields, &requested).collect::<Vec<_>>();
657
658 assert_eq!(selected, [field("org.a"), field("org.z")]);
659 }
660 }
661
662 #[test]
663 fn repeated_base_subjects_share_one_selection() {
664 let alice = user_id!("@alice:example.com");
665 let bob = user_id!("@bob:example.com");
666 let changes = [bob, alice, bob, alice]
667 .into_iter()
668 .map(ToOwned::to_owned)
669 .map(Input::Base)
670 .collect::<Candidates>();
671
672 assert_eq!(changes.0.len(), 2);
673 assert!(changes.0.values().all(is_base));
674 assert_eq!(changes.0.into_keys().collect::<Vec<_>>(), [alice, bob]);
675 }
676
677 #[test]
678 fn room_candidate_merges_preserve_union_and_base() {
679 let alice = user_id!("@alice:example.com");
680 let bob = user_id!("@bob:example.com");
681
682 for order in [[0, 1, 2], [2, 0, 1], [1, 2, 0]] {
683 let rooms = order.map(|index| match index {
684 | 0 => [(alice, "org.a"), (bob, "org.a")]
685 .into_iter()
686 .map(own_change)
687 .collect::<Candidates>(),
688 | 1 => [(alice, "org.z"), (bob, "org.z")]
689 .into_iter()
690 .map(own_change)
691 .collect(),
692 | _ => once(Input::Base(alice.to_owned())).collect(),
693 });
694
695 let changes: Candidates = rooms.into_iter().map(Input::Collected).collect();
696
697 assert_eq!(changes.0.len(), 2);
698 assert!(is_base(changes.0.get(alice).expect("alice base")));
699
700 let bob = changes.0.into_values().last().expect("bob delta");
701 let fields = selected_fields(bob, &[]).collect::<Vec<_>>();
702
703 assert_eq!(fields, [field("org.a"), field("org.z")]);
704 }
705 }
706
707 #[test]
708 fn collected_seed_preserves_base_dominance_in_either_order() {
709 let user = user_id!("@alice:example.com");
710
711 for seed_first in [true, false] {
712 let seed = [(user, "org.a"), (user, "org.z")]
713 .into_iter()
714 .map(own_change)
715 .collect();
716
717 let inputs = match seed_first {
718 | true => [Input::Collected(seed), Input::Base(user.to_owned())],
719 | false => [Input::Base(user.to_owned()), Input::Collected(seed)],
720 };
721
722 let changes: Candidates = inputs.into_iter().collect();
723
724 assert_eq!(changes.0.len(), 1);
725 assert!(is_base(changes.0.get(user).expect("base survives seed")));
726 }
727 }
728
729 #[test]
730 fn only_the_filtered_fields_are_carried() {
731 let requested = [field("m.status"), field("displayname")];
732
733 assert!(was_requested(&requested, "m.status"));
734 assert!(was_requested(&requested, "displayname"));
735 assert!(!was_requested(&requested, "avatar_url"));
736 assert!(!was_requested(&[], "m.status"));
737 }
738
739 #[test]
740 fn an_absent_field_reads_as_a_removal() {
741 let updates = [
742 (field("m.status"), Ok(Some(json!({"emoji": "🏊"})))),
743 (field("displayname"), Ok(None)),
744 ]
745 .into_iter()
746 .map(field_update)
747 .collect::<Result<Updates>>()
748 .expect("readable fields");
749
750 assert_eq!(updates.get(&field("m.status")), Some(&json!({"emoji": "🏊"})));
751 assert_eq!(updates.get(&field("displayname")), Some(&Value::Null));
752 field_update((field("avatar_url"), Err!("unreadable")))
753 .expect_err("unreadable fields abort collection");
754 }
755
756 type MemberRow<'a> = (&'a str, &'a str, &'a str);
758
759 fn events<T>(rows: &[MemberRow<'_>]) -> Vec<Raw<T>> {
760 rows.iter()
761 .map(|(event_type, state_key, membership)| {
762 json!({
763 "type": event_type,
764 "state_key": state_key,
765 "content": { "membership": membership },
766 })
767 })
768 .map(|event| Raw::new(&event).expect("event serializes"))
769 .map(|event| event.cast_ref_unchecked::<T>().clone())
770 .collect()
771 }
772
773 #[test]
774 fn only_present_members_are_witnessed() {
775 let room = JoinedRoom {
776 state: State::Before(StateEvents {
777 events: events(&[
778 ("m.room.member", "@alice:example.com", "join"),
779 ("m.room.topic", "", "join"),
780 ("m.room.member", "@gone:example.com", "leave"),
781 ("m.room.member", "@banned:example.com", "ban"),
782 ("m.room.member", "@asked:example.com", "invite"),
783 ]),
784 }),
785 ..Default::default()
786 };
787
788 assert_eq!(members(&room), ["@alice:example.com", "@asked:example.com"]);
789 }
790
791 #[test]
792 fn a_state_key_needing_unescaping_is_witnessed() {
793 let room = JoinedRoom {
796 state: State::Before(StateEvents {
797 events: events(&[("m.room.member", r"@od\d:example.com", "join")]),
798 }),
799 ..Default::default()
800 };
801
802 assert_eq!(members(&room), [r"@od\d:example.com"]);
803 }
804
805 #[test]
806 fn a_member_joining_inside_the_timeline_is_witnessed() {
807 let room = JoinedRoom {
809 timeline: Timeline {
810 events: events(&[("m.room.member", "@late:example.com", "join")]),
811 ..Default::default()
812 },
813 ..Default::default()
814 };
815
816 assert_eq!(members(&room), ["@late:example.com"]);
817 }
818}