tuwunel_api/client/sync/v5/extensions/
profiles.rs1use futures::{StreamExt, TryStreamExt};
2use itertools::Itertools;
3use ruma::{
4 OwnedUserId, RoomId, UserId,
5 api::client::sync::sync_events::v5::response::{Profiles, Room as ResponseRoom},
6 events::{StateEventType, room::member::MembershipState},
7 profile::{ProfileFieldName, UserProfileChanges, UserProfileUpdate},
8 serde::Raw,
9};
10use serde_json::Value;
11use tuwunel_core::{
12 Result,
13 utils::{
14 BoolExt, IterStream,
15 stream::{BroadbandExt, TryReadyExt},
16 },
17};
18use tuwunel_service::{Services, sync::Connection};
19
20use super::{
21 super::{range::Results, rooms::merged_room_details},
22 SyncInfo, Window, selector,
23};
24use crate::client::sync::profiles::{Changes, Fields, fold_change, read_field, visible};
25
26#[tracing::instrument(name = "profiles", level = "trace", skip_all)]
31pub(super) async fn collect(
32 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
33 conn: &Connection,
34 window: &Window,
35 ranges: &Results,
36) -> Result<Profiles> {
37 let requested = conn.extensions.profiles.fields.as_deref();
38 if requested.is_some_and(<[_]>::is_empty) {
39 return Ok(Profiles::default());
40 }
41
42 let bases = room_bases(services, conn, window, ranges).await?;
43 let changes = bases
44 .chain(
45 conn.own_profile_owed()
46 .then_some(sender_user.to_owned()),
47 )
48 .sorted_unstable()
49 .dedup()
50 .stream()
51 .broad_then(|user_id| base(services, sender_user, user_id, requested))
52 .ready_try_filter_map(Result::Ok)
53 .try_collect()
54 .await?;
55
56 let changes = services
57 .profile
58 .try_profile_changed(sender_user, conn.globalsince, Some(conn.next_batch))
59 .ready_try_filter(|(_, field)| was_requested(requested, field))
60 .ready_try_fold(changes, |changes, change| Ok(fold_change(changes, change)))
61 .await?;
62
63 let changes = window
64 .keys()
65 .merge(conn.rooms.keys())
66 .dedup()
67 .filter(|_| conn.globalsince != 0)
68 .try_stream()
69 .try_fold(changes, async |changes, room_id| {
70 fold_room(changes, services, conn, room_id, requested).await
71 })
72 .await?;
73
74 let users = changes
75 .into_iter()
76 .stream()
77 .broad_then(|(user_id, fields)| collect_user(services, sender_user, user_id, fields))
78 .ready_try_filter_map(Result::Ok)
79 .try_collect()
80 .await?;
81
82 Ok(Profiles { users })
83}
84
85#[tracing::instrument(level = "trace", skip_all)]
86async fn room_bases(
87 services: &Services,
88 conn: &Connection,
89 window: &Window,
90 ranges: &Results,
91) -> Result<impl Iterator<Item = OwnedUserId>> {
92 let config = &conn.extensions.profiles;
93
94 let subjects = selector(
95 conn,
96 window,
97 config.lists.as_ref().map(|lists| lists.iter()),
98 config.rooms.as_ref().map(|rooms| rooms.iter()),
99 )
100 .filter_map(|room_id| {
101 ranges
102 .payload(room_id)
103 .map(|room| (room_id, room))
104 })
105 .filter(|(_, room)| room.initial.unwrap_or(false) || conn.own_profile_owed())
106 .try_stream()
107 .try_fold(Vec::new(), async |bases, (room_id, room)| {
108 let bases = extend_subjects(bases, subjects(room));
109
110 let Some(selected) = window.get(room_id) else {
111 return Ok(bases);
112 };
113
114 let (_, state) = merged_room_details(conn, &selected.lists, room_id);
115 let lazy = state
116 .iter()
117 .any(|(kind, key)| kind == &StateEventType::RoomMember && key == "$LAZY");
118
119 let full = state.iter().any(|(kind, key)| {
120 (kind == &StateEventType::RoomMember || kind == &StateEventType::from("*"))
121 && key == "*"
122 });
123
124 if (lazy && !full) || room.membership != Some(MembershipState::Join) {
125 return Ok(bases);
126 }
127
128 services
129 .state_cache
130 .room_members_checked(room_id)
131 .map_ok(ToOwned::to_owned)
132 .ready_try_fold(bases, |bases, user_id| Ok(extend_subjects(bases, [user_id])))
133 .await
134 })
135 .await?;
136
137 Ok(subjects.into_iter())
138}
139
140fn extend_subjects(
141 mut subjects: Vec<OwnedUserId>,
142 additional: impl IntoIterator<Item = OwnedUserId>,
143) -> Vec<OwnedUserId> {
144 subjects.extend(additional);
145 subjects
146}
147
148fn subjects(room: &ResponseRoom) -> impl Iterator<Item = OwnedUserId> + '_ {
149 let senders = room
150 .timeline
151 .iter()
152 .filter_map(|event| event.get_field("sender").ok().flatten());
153
154 let members = room
155 .timeline
156 .iter()
157 .filter_map(member)
158 .chain(room.required_state.iter().filter_map(member));
159
160 let heroes = room
161 .heroes
162 .iter()
163 .flatten()
164 .map(|hero| hero.user_id.clone());
165
166 senders.chain(members).chain(heroes)
167}
168
169fn member<T>(event: &Raw<T>) -> Option<OwnedUserId> {
170 event
171 .get_field("type")
172 .ok()
173 .flatten()
174 .filter(|kind: &StateEventType| kind == &StateEventType::RoomMember)
175 .and_then(|_| event.get_field("state_key").ok().flatten())
176}
177
178#[tracing::instrument(level = "trace", skip_all)]
179async fn base(
180 services: &Services,
181 sender_user: &UserId,
182 user_id: OwnedUserId,
183 requested: Option<&[ProfileFieldName]>,
184) -> Result<Option<(OwnedUserId, Fields)>> {
185 if !visible(services, sender_user, &user_id).await? {
186 return Ok(None);
187 }
188
189 let fields: Fields = match requested {
190 | Some(fields) => fields.iter().cloned().collect(),
191 | None =>
192 services
193 .profile
194 .try_profile_field_names(&user_id)
195 .try_collect()
196 .await?,
197 };
198
199 Ok(fields
200 .is_empty()
201 .is_false()
202 .then_some((user_id, fields)))
203}
204
205async fn fold_room(
206 changes: Changes,
207 services: &Services,
208 conn: &Connection,
209 room_id: &RoomId,
210 requested: Option<&[ProfileFieldName]>,
211) -> Result<Changes> {
212 services
213 .profile
214 .try_room_profile_changed(room_id, changes_from(conn, room_id), Some(conn.next_batch))
215 .ready_try_filter(|(_, field)| was_requested(requested, field))
216 .ready_try_fold(changes, |changes, change| Ok(fold_change(changes, change)))
217 .await
218}
219
220fn changes_from(conn: &Connection, room_id: &RoomId) -> u64 {
221 conn.rooms
222 .get(room_id)
223 .is_some_and(|room| room.roomsince.gt(&0) && conn.profiles_fields_owed().is_false())
224 .then_some(conn.globalsince)
225 .unwrap_or_default()
226}
227
228fn was_requested(requested: Option<&[ProfileFieldName]>, field: &str) -> bool {
229 requested.is_none_or(|fields| fields.iter().any(|name| name.as_str() == field))
230}
231
232async fn collect_user(
233 services: &Services,
234 sender_user: &UserId,
235 user_id: OwnedUserId,
236 fields: Fields,
237) -> Result<Option<(OwnedUserId, UserProfileUpdate)>> {
238 if !visible(services, sender_user, &user_id).await? {
239 return Ok(None);
240 }
241
242 let update = read_update(services, &user_id, fields).await?;
243
244 Ok(Some((user_id, update)))
245}
246
247async fn read_update(
248 services: &Services,
249 user_id: &UserId,
250 fields: Fields,
251) -> Result<UserProfileUpdate> {
252 let changes = fields
253 .into_iter()
254 .stream()
255 .then(|name| read_field(services, user_id, name))
256 .map(|(name, value)| value.map(|value| (name, value)))
257 .ready_try_fold(UserProfileChanges::new(), |changes, field| {
258 Ok(fold_field(changes, field))
259 })
260 .await?;
261
262 Ok(UserProfileUpdate::Updated(changes))
263}
264
265fn fold_field(
266 mut changes: UserProfileChanges,
267 (name, value): (ProfileFieldName, Option<Value>),
268) -> UserProfileChanges {
269 match value {
270 | None => changes.removed.push(name),
271 | Some(value) => {
272 changes.updated.insert(name, value);
273 },
274 }
275
276 changes
277}
278
279#[cfg(test)]
280mod tests {
281 use ruma::room_id;
282 use tuwunel_service::sync::{Connection, Room};
283
284 use super::changes_from;
285
286 #[test]
287 fn a_widened_field_set_replays_a_known_room() {
288 let room_id = room_id!("!known:example.com");
289 let mut conn = Connection {
290 globalsince: 7,
291 rooms: [(room_id.to_owned(), Room { roomsince: 3, ..Default::default() })].into(),
292 ..Default::default()
293 };
294
295 conn.extensions.profiles.enabled = Some(true);
296
297 assert_eq!(changes_from(&conn, room_id), 7);
298
299 conn.profiles_fields_widened = true;
300
301 assert_eq!(changes_from(&conn, room_id), 0);
302
303 conn.own_profile_since = 7;
304
305 assert_eq!(changes_from(&conn, room_id), 7);
306 }
307}