diff --git a/crates/matrix-synapse/src/lib.rs b/crates/matrix-synapse/src/lib.rs index fb3b5f08..54575700 100644 --- a/crates/matrix-synapse/src/lib.rs +++ b/crates/matrix-synapse/src/lib.rs @@ -175,6 +175,37 @@ impl HomeserverConnection for SynapseConnection { }) } + #[tracing::instrument( + name = "homeserver.is_localpart_available", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Display), + )] + async fn is_localpart_available(&self, localpart: &str) -> Result { + let mut client = self + .http_client_factory + .client("homeserver.is_localpart_available"); + + let request = self + .get(&format!( + "_synapse/admin/v1/username_available?username={localpart}" + )) + .body(EmptyBody::new())?; + + let response = client.ready().await?.call(request).await?; + + match response.status() { + StatusCode::OK => Ok(true), + StatusCode::BAD_REQUEST => Ok(false), + _ => Err(anyhow::anyhow!( + "Failed to query localpart availability from Synapse" + )), + } + } + #[tracing::instrument( name = "homeserver.provision_user", skip_all, diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 6953ef3d..5e1589e3 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -219,6 +219,17 @@ pub trait HomeserverConnection: Send + Sync { /// be provisioned. async fn provision_user(&self, request: &ProvisionRequest) -> Result; + /// Check whether a given username is available on the homeserver. + /// + /// # Parameters + /// + /// * `localpart` - The localpart to check. + /// + /// # Errors + /// + /// Returns an error if the homeserver is unreachable. + async fn is_localpart_available(&self, localpart: &str) -> Result; + /// Create a device for a user on the homeserver. /// /// # Parameters @@ -312,6 +323,10 @@ impl HomeserverConnection for &T (**self).provision_user(request).await } + async fn is_localpart_available(&self, localpart: &str) -> Result { + (**self).is_localpart_available(localpart).await + } + async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), Self::Error> { (**self).create_device(mxid, device_id).await } diff --git a/crates/matrix/src/mock.rs b/crates/matrix/src/mock.rs index d6e9ffe5..7a67c550 100644 --- a/crates/matrix/src/mock.rs +++ b/crates/matrix/src/mock.rs @@ -34,6 +34,7 @@ struct MockUser { pub struct HomeserverConnection { homeserver: String, users: RwLock>, + reserved_localparts: RwLock>, } impl HomeserverConnection { @@ -45,8 +46,13 @@ impl HomeserverConnection { Self { homeserver: homeserver.into(), users: RwLock::new(HashMap::new()), + reserved_localparts: RwLock::new(HashSet::new()), } } + + pub async fn reserve_localpart(&self, localpart: &'static str) { + self.reserved_localparts.write().await.insert(localpart); + } } #[async_trait] @@ -98,6 +104,16 @@ impl crate::HomeserverConnection for HomeserverConnection { Ok(inserted) } + async fn is_localpart_available(&self, localpart: &str) -> Result { + if self.reserved_localparts.read().await.contains(localpart) { + return Ok(false); + } + + let mxid = self.mxid(localpart); + let users = self.users.read().await; + Ok(!users.contains_key(&mxid)) + } + async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), Self::Error> { let mut users = self.users.write().await; let user = users.get_mut(mxid).context("User not found")?; @@ -200,5 +216,14 @@ mod tests { // XXX: there is no API to query devices yet in the trait // Delete the device assert!(conn.delete_device(mxid, device).await.is_ok()); + + // The user we just created should be not available + assert!(!conn.is_localpart_available("test").await.unwrap()); + // But another user should be + assert!(conn.is_localpart_available("alice").await.unwrap()); + + // Reserve the localpart, it should not be available anymore + conn.reserve_localpart("alice").await; + assert!(!conn.is_localpart_available("alice").await.unwrap()); } }