Skip to main content

tuwunel_service/profile/
remote.rs

1use futures::TryStreamExt;
2use ruma::{
3	UserId,
4	api::federation::query::get_profile_information::v1::{Request, Response},
5	profile::ProfileFieldName,
6};
7use serde_json::Value;
8use tuwunel_core::{Result, implement, smallvec::SmallVec, utils::stream::TryReadyExt};
9
10use super::{Propagation, Service};
11
12type Removed = SmallVec<[ProfileFieldName; 1]>;
13
14type Fields = Vec<(ProfileFieldName, Option<Value>)>;
15
16/// Replaces a remote user's cached profile with the one their server serves.
17///
18/// Unlike `fetch_remote_profile`, which only adds and overwrites, a cached
19/// field missing from the response is removed, so a value the remote user has
20/// since deleted stops reaching clients. Returns the names of the removed
21/// fields.
22#[implement(Service)]
23#[tracing::instrument(
24	level = "debug",
25	skip_all,
26	fields(
27		%user_id,
28	),
29)]
30pub async fn mirror_remote_profile(&self, user_id: &UserId) -> Result<Removed> {
31	assert!(
32		!self.services.globals.user_is_local(user_id),
33		"mirror remote profile called with a local user"
34	);
35
36	let request = Request { user_id: user_id.to_owned(), field: None };
37	let response = self
38		.services
39		.federation
40		.execute(user_id.server_name(), request)
41		.await?;
42
43	self.mirror_profile(user_id, response).await
44}
45
46/// Stores a profile response as the user's complete cached profile.
47///
48/// Every returned field is written and every cached field the response omits
49/// is deleted in one logged write under the profile lock, so no concurrent
50/// write interleaves and connected clients see the removals.
51#[implement(Service)]
52pub(super) async fn mirror_profile(
53	&self,
54	user_id: &UserId,
55	response: Response,
56) -> Result<Removed> {
57	let profile_lock = self.mutex.lock(user_id).await;
58	let removed: Removed = self
59		.try_profile_field_names(user_id)
60		.ready_try_filter(|name| response.get(name.as_str()).is_none())
61		.try_collect()
62		.await?;
63
64	let fields: Fields = response
65		.into_iter()
66		.map(|(name, value)| (name.into(), Some(value)))
67		.chain(removed.iter().cloned().map(|name| (name, None)))
68		.collect();
69
70	self.set_profile_keys_locked(&profile_lock, user_id, &fields, Some(Propagation::None))
71		.await?;
72
73	Ok(removed)
74}