From b28a2fad9732c7d6241d8606c3dd9939ff3d760c Mon Sep 17 00:00:00 2001 From: strawberry Date: Thu, 11 Jan 2024 20:39:37 -0500 Subject: [PATCH] feat: keep track of remote profiles for user directory and local requests Signed-off-by: strawberry --- src/api/client_server/membership.rs | 4 +- src/api/client_server/profile.rs | 65 +++++++++++++++++++++++----- src/api/server_server.rs | 2 +- src/service/rooms/state/mod.rs | 23 +++++----- src/service/rooms/state_cache/mod.rs | 34 +++++++++++++-- src/service/rooms/timeline/mod.rs | 18 ++++---- 6 files changed, 110 insertions(+), 36 deletions(-) diff --git a/src/api/client_server/membership.rs b/src/api/client_server/membership.rs index 640d8048..70b9c321 100644 --- a/src/api/client_server/membership.rs +++ b/src/api/client_server/membership.rs @@ -1426,7 +1426,7 @@ pub async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option, ) -> Result { - if body.user_id.server_name() != services().globals.server_name() { + if (services().users.exists(&body.user_id)?) + && (body.user_id.server_name() != services().globals.server_name()) + { let response = services() .sending .send_federation_request( @@ -123,6 +126,18 @@ pub async fn get_displayname_route( ) .await?; + // Create and update our local copy of the user + services().users.create(&body.user_id, None)?; + services() + .users + .set_displayname(&body.user_id, response.displayname.clone())?; + services() + .users + .set_avatar_url(&body.user_id, response.avatar_url)?; + services() + .users + .set_blurhash(&body.user_id, response.blurhash)?; + return Ok(get_display_name::v3::Response { displayname: response.displayname, }); @@ -225,15 +240,18 @@ pub async fn set_avatar_url_route( Ok(set_avatar_url::v3::Response {}) } -/// # `GET /_matrix/client/r0/profile/{userId}/avatar_url` +/// # `GET /_matrix/client/v3/profile/{userId}/avatar_url` /// /// Returns the avatar_url and blurhash of the user. /// -/// - If user is on another server: Fetches avatar_url and blurhash over federation +/// - If user is on another server and we do not have a local copy already +/// fetch avatar_url and blurhash over federation pub async fn get_avatar_url_route( body: Ruma, ) -> Result { - if body.user_id.server_name() != services().globals.server_name() { + if (services().users.exists(&body.user_id)?) + && (body.user_id.server_name() != services().globals.server_name()) + { let response = services() .sending .send_federation_request( @@ -245,6 +263,18 @@ pub async fn get_avatar_url_route( ) .await?; + // Create and update our local copy of the user + services().users.create(&body.user_id, None)?; + services() + .users + .set_displayname(&body.user_id, response.displayname)?; + services() + .users + .set_avatar_url(&body.user_id, response.avatar_url.clone())?; + services() + .users + .set_blurhash(&body.user_id, response.blurhash.clone())?; + return Ok(get_avatar_url::v3::Response { avatar_url: response.avatar_url, blurhash: response.blurhash, @@ -257,15 +287,18 @@ pub async fn get_avatar_url_route( }) } -/// # `GET /_matrix/client/r0/profile/{userId}` +/// # `GET /_matrix/client/v3/profile/{userId}` /// /// Returns the displayname, avatar_url and blurhash of the user. /// -/// - If user is on another server: Fetches profile over federation +/// - If user is on another server and we do not have a local copy already, +/// fetch profile over federation. pub async fn get_profile_route( body: Ruma, ) -> Result { - if body.user_id.server_name() != services().globals.server_name() { + if (services().users.exists(&body.user_id)?) + && (body.user_id.server_name() != services().globals.server_name()) + { let response = services() .sending .send_federation_request( @@ -277,6 +310,18 @@ pub async fn get_profile_route( ) .await?; + // Create and update our local copy of the user + services().users.create(&body.user_id, None)?; + services() + .users + .set_displayname(&body.user_id, response.displayname.clone())?; + services() + .users + .set_avatar_url(&body.user_id, response.avatar_url.clone())?; + services() + .users + .set_blurhash(&body.user_id, response.blurhash.clone())?; + return Ok(get_profile::v3::Response { displayname: response.displayname, avatar_url: response.avatar_url, @@ -285,7 +330,7 @@ pub async fn get_profile_route( } if !services().users.exists(&body.user_id)? { - // Return 404 if this user doesn't exist + // Return 404 if this user doesn't exist and we couldn't fetch it over federation return Err(Error::BadRequest( ErrorKind::NotFound, "Profile was not found.", diff --git a/src/api/server_server.rs b/src/api/server_server.rs index 7a993365..b0113352 100644 --- a/src/api/server_server.rs +++ b/src/api/server_server.rs @@ -1883,7 +1883,7 @@ pub async fn create_invite_route( .update_membership( &body.room_id, &invited_user, - MembershipState::Invite, + RoomMemberEventContent::new(MembershipState::Invite), &sender, Some(invite_state), true, diff --git a/src/service/rooms/state/mod.rs b/src/service/rooms/state/mod.rs index 282b0fe8..a056a065 100644 --- a/src/service/rooms/state/mod.rs +++ b/src/service/rooms/state/mod.rs @@ -8,14 +8,13 @@ pub use data::Data; use ruma::{ api::client::error::ErrorKind, events::{ - room::{create::RoomCreateEventContent, member::MembershipState}, + room::{create::RoomCreateEventContent, member::RoomMemberEventContent}, AnyStrippedStateEvent, StateEventType, TimelineEventType, }, serde::Raw, state_res::{self, StateMap}, EventId, OwnedEventId, RoomId, RoomVersionId, UserId, }; -use serde::Deserialize; use tokio::sync::MutexGuard; use tracing::warn; @@ -59,14 +58,9 @@ impl Service { match pdu.kind { TimelineEventType::RoomMember => { - #[derive(Deserialize)] - struct ExtractMembership { - membership: MembershipState, - } - - let membership = - match serde_json::from_str::(pdu.content.get()) { - Ok(e) => e.membership, + let membership_event = + match serde_json::from_str::(pdu.content.get()) { + Ok(e) => e, Err(_) => continue, }; @@ -83,7 +77,14 @@ impl Service { services() .rooms .state_cache - .update_membership(room_id, &user_id, membership, &pdu.sender, None, false) + .update_membership( + room_id, + &user_id, + membership_event, + &pdu.sender, + None, + false, + ) .await?; } TimelineEventType::SpaceChild => { diff --git a/src/service/rooms/state_cache/mod.rs b/src/service/rooms/state_cache/mod.rs index d106b7ac..f3b1a823 100644 --- a/src/service/rooms/state_cache/mod.rs +++ b/src/service/rooms/state_cache/mod.rs @@ -4,10 +4,14 @@ use std::{collections::HashSet, sync::Arc}; pub use data::Data; use ruma::{ + api::federation::{self, query::get_profile_information::v1::ProfileField}, events::{ direct::DirectEvent, ignored_user_list::IgnoredUserListEvent, - room::{create::RoomCreateEventContent, member::MembershipState}, + room::{ + create::RoomCreateEventContent, + member::{MembershipState, RoomMemberEventContent}, + }, AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, }, @@ -29,15 +33,39 @@ impl Service { &self, room_id: &RoomId, user_id: &UserId, - membership: MembershipState, + membership_event: RoomMemberEventContent, sender: &UserId, last_state: Option>>, update_joined_count: bool, ) -> Result<()> { + let membership = membership_event.membership; // Keep track what remote users exist by adding them as "deactivated" users if user_id.server_name() != services().globals.server_name() { services().users.create(user_id, None)?; - // TODO: displayname, avatar url + // Try to update our local copy of the user if ours does not match + if ((services().users.displayname(user_id)? != membership_event.displayname) + || (services().users.avatar_url(user_id)? != membership_event.avatar_url) + || (services().users.blurhash(user_id)? != membership_event.blurhash)) + && (membership != MembershipState::Leave) + { + let response = services() + .sending + .send_federation_request( + user_id.server_name(), + federation::query::get_profile_information::v1::Request { + user_id: user_id.into(), + field: Some(ProfileField::AvatarUrl), + }, + ) + .await?; + services() + .users + .set_displayname(user_id, response.displayname.clone())?; + services() + .users + .set_avatar_url(user_id, response.avatar_url)?; + services().users.set_blurhash(user_id, response.blurhash)?; + }; } match &membership { diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index e4ba1acf..964f5844 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -18,7 +18,9 @@ use ruma::{ events::{ push_rules::PushRulesEvent, room::{ - create::RoomCreateEventContent, encrypted::Relation, member::MembershipState, + create::RoomCreateEventContent, + encrypted::Relation, + member::{MembershipState, RoomMemberEventContent}, power_levels::RoomPowerLevelsEventContent, }, GlobalAccountDataEventType, StateEventType, TimelineEventType, @@ -453,17 +455,15 @@ impl Service { } TimelineEventType::RoomMember => { if let Some(state_key) = &pdu.state_key { - #[derive(Deserialize)] - struct ExtractMembership { - membership: MembershipState, - } - // if the state_key fails let target_user_id = UserId::parse(state_key.clone()) .expect("This state_key was previously validated"); - let content = serde_json::from_str::(pdu.content.get()) - .map_err(|_| Error::bad_database("Invalid content in pdu."))?; + let content = serde_json::from_str::(pdu.content.get()) + .map_err(|e| { + error!("Invalid room member event content in pdu: {e}"); + Error::bad_database("Invalid room member event content in pdu.") + })?; let invite_state = match content.membership { MembershipState::Invite => { @@ -481,7 +481,7 @@ impl Service { .update_membership( &pdu.room_id, &target_user_id, - content.membership, + content, &pdu.sender, invite_state, true,