From f163ebf3bbaa9656bbf1d78355220e1d859b17c7 Mon Sep 17 00:00:00 2001 From: strawberry Date: Sat, 7 Sep 2024 09:26:50 -0400 Subject: [PATCH] implement MSC4133 only with MSC4175 for GET/PUT/DELETE Signed-off-by: strawberry --- src/api/client/profile.rs | 32 +++++++--- src/api/client/unstable.rs | 128 ++++++++++++++++++++++++++++++++++++- src/api/router.rs | 3 + src/api/server/query.rs | 3 + src/database/maps.rs | 1 + src/service/users/data.rs | 53 +++++++++++++++ src/service/users/mod.rs | 7 ++ 7 files changed, 217 insertions(+), 10 deletions(-) diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs index 71d49cd8..0e1e0f0a 100644 --- a/src/api/client/profile.rs +++ b/src/api/client/profile.rs @@ -1,5 +1,5 @@ use axum::extract::State; -use conduit::{pdu::PduBuilder, warn, Error, Result}; +use conduit::{pdu::PduBuilder, warn, Err, Error, Result}; use ruma::{ api::{ client::{ @@ -26,20 +26,25 @@ pub(crate) async fn set_displayname_route( State(services): State, body: Ruma, ) -> Result { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + + if *sender_user != body.user_id && body.appservice_info.is_none() { + return Err!(Request(Forbidden("You cannot update the profile of another user"))); + } + let all_joined_rooms: Vec = services .rooms .state_cache - .rooms_joined(sender_user) + .rooms_joined(&body.user_id) .filter_map(Result::ok) .collect(); - update_displayname(&services, sender_user.clone(), body.displayname.clone(), all_joined_rooms).await?; + update_displayname(&services, body.user_id.clone(), body.displayname.clone(), all_joined_rooms).await?; if services.globals.allow_local_presence() { // Presence update services .presence - .ping_presence(sender_user, &PresenceState::Online)?; + .ping_presence(&body.user_id, &PresenceState::Online)?; } Ok(set_display_name::v3::Response {}) @@ -110,16 +115,21 @@ pub(crate) async fn set_avatar_url_route( State(services): State, body: Ruma, ) -> Result { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + + if *sender_user != body.user_id && body.appservice_info.is_none() { + return Err!(Request(Forbidden("You cannot update the profile of another user"))); + } + let all_joined_rooms: Vec = services .rooms .state_cache - .rooms_joined(sender_user) + .rooms_joined(&body.user_id) .filter_map(Result::ok) .collect(); update_avatar_url( &services, - sender_user.clone(), + body.user_id.clone(), body.avatar_url.clone(), body.blurhash.clone(), all_joined_rooms, @@ -130,7 +140,7 @@ pub(crate) async fn set_avatar_url_route( // Presence update services .presence - .ping_presence(sender_user, &PresenceState::Online)?; + .ping_presence(&body.user_id, &PresenceState::Online)?; } Ok(set_avatar_url::v3::Response {}) @@ -196,7 +206,7 @@ pub(crate) async fn get_avatar_url_route( /// # `GET /_matrix/client/v3/profile/{userId}` /// -/// Returns the displayname, avatar_url and blurhash of the user. +/// Returns the displayname, avatar_url, blurhash, and tz of the user. /// /// - If user is on another server and we do not have a local copy already, /// fetch profile over federation. @@ -232,11 +242,16 @@ pub(crate) async fn get_profile_route( .users .set_blurhash(&body.user_id, response.blurhash.clone()) .await?; + services + .users + .set_timezone(&body.user_id, response.tz.clone()) + .await?; return Ok(get_profile::v3::Response { displayname: response.displayname, avatar_url: response.avatar_url, blurhash: response.blurhash, + tz: response.tz, }); } } @@ -251,6 +266,7 @@ pub(crate) async fn get_profile_route( avatar_url: services.users.avatar_url(&body.user_id)?, blurhash: services.users.blurhash(&body.user_id)?, displayname: services.users.displayname(&body.user_id)?, + tz: services.users.timezone(&body.user_id)?, }) } diff --git a/src/api/client/unstable.rs b/src/api/client/unstable.rs index 89650951..d672ad46 100644 --- a/src/api/client/unstable.rs +++ b/src/api/client/unstable.rs @@ -1,9 +1,18 @@ use axum::extract::State; use axum_client_ip::InsecureClientIp; -use conduit::warn; +use conduit::{warn, Err}; use ruma::{ - api::client::{error::ErrorKind, membership::mutual_rooms, room::get_summary}, + api::{ + client::{ + error::ErrorKind, + membership::mutual_rooms, + profile::{delete_timezone_key, get_timezone_key, set_timezone_key}, + room::get_summary, + }, + federation, + }, events::room::member::MembershipState, + presence::PresenceState, OwnedRoomId, }; @@ -161,3 +170,118 @@ pub(crate) async fn get_room_summary( .unwrap_or_else(|_e| None), }) } + +/// # `DELETE /_matrix/client/unstable/uk.tcpip.msc4133/profile/:user_id/us.cloke.msc4175.tz` +/// +/// Deletes the `tz` (timezone) of a user, as per MSC4133 and MSC4175. +/// +/// - Also makes sure other users receive the update using presence EDUs +pub(crate) async fn delete_timezone_key_route( + State(services): State, body: Ruma, +) -> Result { + let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + + if *sender_user != body.user_id && body.appservice_info.is_none() { + return Err!(Request(Forbidden("You cannot update the profile of another user"))); + } + + services.users.set_timezone(&body.user_id, None).await?; + + if services.globals.allow_local_presence() { + // Presence update + services + .presence + .ping_presence(&body.user_id, &PresenceState::Online)?; + } + + Ok(delete_timezone_key::unstable::Response {}) +} + +/// # `PUT /_matrix/client/unstable/uk.tcpip.msc4133/profile/:user_id/us.cloke.msc4175.tz` +/// +/// Updates the `tz` (timezone) of a user, as per MSC4133 and MSC4175. +/// +/// - Also makes sure other users receive the update using presence EDUs +pub(crate) async fn set_timezone_key_route( + State(services): State, body: Ruma, +) -> Result { + let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + + if *sender_user != body.user_id && body.appservice_info.is_none() { + return Err!(Request(Forbidden("You cannot update the profile of another user"))); + } + + services + .users + .set_timezone(&body.user_id, body.tz.clone()) + .await?; + + if services.globals.allow_local_presence() { + // Presence update + services + .presence + .ping_presence(&body.user_id, &PresenceState::Online)?; + } + + Ok(set_timezone_key::unstable::Response {}) +} + +/// # `GET /_matrix/client/unstable/uk.tcpip.msc4133/profile/:user_id/us.cloke.msc4175.tz` +/// +/// Returns the `timezone` of the user as per MSC4133 and MSC4175. +/// +/// - If user is on another server and we do not have a local copy already fetch +/// `timezone` over federation +pub(crate) async fn get_timezone_key_route( + State(services): State, body: Ruma, +) -> Result { + if !services.globals.user_is_local(&body.user_id) { + // Create and update our local copy of the user + if let Ok(response) = services + .sending + .send_federation_request( + body.user_id.server_name(), + federation::query::get_profile_information::v1::Request { + user_id: body.user_id.clone(), + field: None, // we want the full user's profile to update locally as well + }, + ) + .await + { + if !services.users.exists(&body.user_id)? { + services.users.create(&body.user_id, None)?; + } + + services + .users + .set_displayname(&body.user_id, response.displayname.clone()) + .await?; + services + .users + .set_avatar_url(&body.user_id, response.avatar_url.clone()) + .await?; + services + .users + .set_blurhash(&body.user_id, response.blurhash.clone()) + .await?; + services + .users + .set_timezone(&body.user_id, response.tz.clone()) + .await?; + + return Ok(get_timezone_key::unstable::Response { + tz: response.tz, + }); + } + } + + if !services.users.exists(&body.user_id)? { + // 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.")); + } + + Ok(get_timezone_key::unstable::Response { + tz: services.users.timezone(&body.user_id)?, + }) +} diff --git a/src/api/router.rs b/src/api/router.rs index 985f803b..77e9a11a 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -22,6 +22,9 @@ use crate::{client, server}; pub fn build(router: Router, server: &Server) -> Router { let config = &server.config; let mut router = router + .ruma_route(client::get_timezone_key_route) + .ruma_route(client::set_timezone_key_route) + .ruma_route(client::delete_timezone_key_route) .ruma_route(client::appservice_ping) .ruma_route(client::get_supported_versions_route) .ruma_route(client::get_register_available_route) diff --git a/src/api/server/query.rs b/src/api/server/query.rs index 5712f46a..45830366 100644 --- a/src/api/server/query.rs +++ b/src/api/server/query.rs @@ -75,6 +75,7 @@ pub(crate) async fn get_profile_information_route( let mut displayname = None; let mut avatar_url = None; let mut blurhash = None; + let mut tz = None; match &body.field { Some(ProfileField::DisplayName) => { @@ -90,6 +91,7 @@ pub(crate) async fn get_profile_information_route( displayname = services.users.displayname(&body.user_id)?; avatar_url = services.users.avatar_url(&body.user_id)?; blurhash = services.users.blurhash(&body.user_id)?; + tz = services.users.timezone(&body.user_id)?; }, } @@ -97,5 +99,6 @@ pub(crate) async fn get_profile_information_route( displayname, avatar_url, blurhash, + tz, }) } diff --git a/src/database/maps.rs b/src/database/maps.rs index 0207b4d4..0e835abf 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -94,6 +94,7 @@ pub const MAPS: &[&str] = &[ "userid_presenceid", "userid_selfsigningkeyid", "userid_usersigningkeyid", + "useridprofilekey_value", "openidtoken_expiresatuserid", "userroomid_highlightcount", "userroomid_invitestate", diff --git a/src/service/users/data.rs b/src/service/users/data.rs index e75a2e93..70785d68 100644 --- a/src/service/users/data.rs +++ b/src/service/users/data.rs @@ -32,6 +32,7 @@ pub struct Data { userid_password: Arc, userid_selfsigningkeyid: Arc, userid_usersigningkeyid: Arc, + useridprofilekey_value: Arc, services: Services, } @@ -64,6 +65,7 @@ impl Data { userid_password: db["userid_password"].clone(), userid_selfsigningkeyid: db["userid_selfsigningkeyid"].clone(), userid_usersigningkeyid: db["userid_usersigningkeyid"].clone(), + useridprofilekey_value: db["useridprofilekey_value"].clone(), services: Services { server: args.server.clone(), globals: args.depend::("globals"), @@ -231,6 +233,57 @@ impl Data { .transpose() } + /// Get the timezone of a user. + pub(super) fn timezone(&self, user_id: &UserId) -> Result> { + // first check the unstable prefix + let mut key = user_id.as_bytes().to_vec(); + key.push(0xFF); + key.extend_from_slice(b"us.cloke.msc4175.tz"); + + let value = self + .useridprofilekey_value + .get(&key)? + .map(|bytes| utils::string_from_bytes(&bytes).map_err(|e| err!(Database("Timezone in db is invalid. {e}")))) + .transpose() + .unwrap(); + + // TODO: transparently migrate unstable key usage to the stable key once MSC4133 + // and MSC4175 are stable, likely a remove/insert in this block + if value.is_none() || value.as_ref().is_some_and(String::is_empty) { + // check the stable prefix + let mut key = user_id.as_bytes().to_vec(); + key.push(0xFF); + key.extend_from_slice(b"m.tz"); + + return self + .useridprofilekey_value + .get(&key)? + .map(|bytes| { + utils::string_from_bytes(&bytes).map_err(|e| err!(Database("Timezone in db is invalid. {e}"))) + }) + .transpose(); + } + + Ok(value) + } + + /// Sets a new timezone or removes it if timezone is None. + pub(super) fn set_timezone(&self, user_id: &UserId, timezone: Option) -> Result<()> { + let mut key = user_id.as_bytes().to_vec(); + key.push(0xFF); + key.extend_from_slice(b"us.cloke.msc4175.tz"); + + // TODO: insert to the stable MSC4175 key when it's stable + if let Some(timezone) = timezone { + self.useridprofilekey_value + .insert(&key, timezone.as_bytes())?; + } else { + self.useridprofilekey_value.remove(&key)?; + } + + Ok(()) + } + /// Sets a new avatar_url or removes it if avatar_url is None. pub(super) fn set_blurhash(&self, user_id: &UserId, blurhash: Option) -> Result<()> { if let Some(blurhash) = blurhash { diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index 35f30a61..544614d4 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -327,6 +327,13 @@ impl Service { /// Get the blurhash of a user. pub fn blurhash(&self, user_id: &UserId) -> Result> { self.db.blurhash(user_id) } + pub fn timezone(&self, user_id: &UserId) -> Result> { self.db.timezone(user_id) } + + /// Sets a new tz or removes it if tz is None. + pub async fn set_timezone(&self, user_id: &UserId, tz: Option) -> Result<()> { + self.db.set_timezone(user_id, tz) + } + /// Sets a new blurhash or removes it if blurhash is None. pub async fn set_blurhash(&self, user_id: &UserId, blurhash: Option) -> Result<()> { self.db.set_blurhash(user_id, blurhash)