diff --git a/Cargo.lock b/Cargo.lock index 96e302c8b95..f1f1890c494 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4651,6 +4651,7 @@ dependencies = [ "ruma-events", "ruma-federation-api", "ruma-html", + "ruma-signatures", "web-time", ] @@ -4744,7 +4745,9 @@ dependencies = [ "headers", "http", "http-auth", + "httparse", "js_int", + "memchr", "mime", "ruma-common", "ruma-events", @@ -4789,6 +4792,21 @@ dependencies = [ "toml 0.8.15", ] +[[package]] +name = "ruma-signatures" +version = "0.17.1" +source = "git+https://github.com/ruma/ruma?rev=e73f302e4df7f5f0511fca1aa43853d4cf8416c8#e73f302e4df7f5f0511fca1aa43853d4cf8416c8" +dependencies = [ + "base64", + "ed25519-dalek", + "pkcs8", + "rand", + "ruma-common", + "serde_json", + "sha2", + "thiserror 2.0.11", +] + [[package]] name = "rusqlite" version = "0.35.0" diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 3ec0e70db36..9b8589e0377 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1618,6 +1618,14 @@ impl Client { .any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_)))) } + /// Get server vendor information from the federation API. + /// + /// This method retrieves information about the server's name and version + /// by calling the `/_matrix/federation/v1/version` endpoint. + pub async fn server_vendor_info(&self) -> Result { + Ok(self.inner.server_vendor_info().await?) + } + /// Subscribe to changes in the media preview configuration. pub async fn subscribe_to_media_preview_config( &self, diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index e730fd8d96e..b6edc4d4374 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -574,6 +574,17 @@ impl ClientBuilder { let sdk_client = inner_builder.build().await?; + // Log server version information at info level. + if let Ok(server_info) = sdk_client.server_vendor_info().await { + tracing::info!( + server_name = %server_info.server_name, + version = %server_info.version, + "Connected to Matrix server" + ); + } else { + tracing::warn!("Could not retrieve server version information"); + } + Ok(Arc::new( Client::new( sdk_client, diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index b1077fac54a..e991ff41f18 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -101,6 +101,7 @@ pin-project-lite.workspace = true rand = { workspace = true, optional = true } ruma = { workspace = true, features = [ "rand", + "federation-api-c", "unstable-msc2448", "unstable-msc4191", "unstable-msc3930", diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 73d41274dd4..1c7314925dd 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -63,6 +63,7 @@ use ruma::{ user_directory::search_users, }, error::FromHttpResponseError, + federation::discovery::get_server_version, FeatureFlag, MatrixVersion, OutgoingRequest, SupportedVersions, }, assign, @@ -151,6 +152,16 @@ pub enum SessionChange { TokensRefreshed, } +/// Information about the server vendor obtained from the federation API. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct ServerVendorInfo { + /// The server name. + pub server_name: String, + /// The server version. + pub version: String, +} + /// An async/await enabled Matrix client. /// /// All of the state is held in an `Arc` so the `Client` can be cloned freely. @@ -521,6 +532,38 @@ impl Client { Ok(res.capabilities) } + /// Get the server vendor information from the federation API. + /// + /// This method calls the `/_matrix/federation/v1/version` endpoint to get + /// both the server's software name and version. + /// + /// # Examples + /// + /// ```no_run + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # async { + /// # let homeserver = Url::parse("http://example.com")?; + /// let client = Client::new(homeserver).await?; + /// + /// let server_info = client.server_vendor_info().await?; + /// println!( + /// "Server: {}, Version: {}", + /// server_info.server_name, server_info.version + /// ); + /// # anyhow::Ok(()) }; + /// ``` + pub async fn server_vendor_info(&self) -> HttpResult { + let res = self.send(get_server_version::v1::Request::new()).await?; + + // Extract server info, using defaults if fields are missing. + let server = res.server.unwrap_or_default(); + let server_name_str = server.name.unwrap_or_else(|| "unknown".to_owned()); + let version = server.version.unwrap_or_else(|| "unknown".to_owned()); + + Ok(ServerVendorInfo { server_name: server_name_str, version }) + } + /// Get a copy of the default request config. /// /// The default request config is what's used when sending requests if no diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 2680d4cb1e3..af050d5fe74 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -68,7 +68,8 @@ pub mod widget; pub use account::Account; pub use authentication::{AuthApi, AuthSession, SessionTokens}; pub use client::{ - sanitize_server_name, Client, ClientBuildError, ClientBuilder, LoopCtrl, SessionChange, + sanitize_server_name, Client, ClientBuildError, ClientBuilder, LoopCtrl, ServerVendorInfo, + SessionChange, }; pub use error::{ Error, HttpError, HttpResult, NotificationSettingsError, RefreshTokenError, Result, diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 5d5d851ef63..eacb17a85cd 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -1377,6 +1377,12 @@ impl MatrixMockServer { ))); self.mock_endpoint(mock, EnablePushRuleEndpoint).expect_default_access_token() } + + /// Create a prebuilt mock for the federation version endpoint. + pub fn mock_federation_version(&self) -> MockEndpoint<'_, FederationVersionEndpoint> { + let mock = Mock::given(method("GET")).and(path("/_matrix/federation/v1/version")); + self.mock_endpoint(mock, FederationVersionEndpoint) + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -3955,3 +3961,25 @@ impl<'a> MockEndpoint<'a, EnablePushRuleEndpoint> { self.ok_empty_json() } } + +/// A prebuilt mock for the federation version endpoint. +pub struct FederationVersionEndpoint; + +impl<'a> MockEndpoint<'a, FederationVersionEndpoint> { + /// Returns a successful response with the given server name and version. + pub fn ok(self, server_name: &str, version: &str) -> MatrixMock<'a> { + let response_body = json!({ + "server": { + "name": server_name, + "version": version + } + }); + self.respond_with(ResponseTemplate::new(200).set_body_json(response_body)) + } + + /// Returns a successful response with empty/missing server information. + pub fn ok_empty(self) -> MatrixMock<'a> { + let response_body = json!({}); + self.respond_with(ResponseTemplate::new(200).set_body_json(response_body)) + } +} diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index 8005dcb6f25..957f3422b0b 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -1490,3 +1490,36 @@ async fn test_room_sync_state_after() { let member = room.get_member_no_sync(user_id!("@invited:localhost")).await.unwrap().unwrap(); assert_eq!(*member.membership(), MembershipState::Leave); } + +#[async_test] +async fn test_server_vendor_info() { + use matrix_sdk::test_utils::mocks::MatrixMockServer; + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + // Mock the federation version endpoint + server.mock_federation_version().ok("Synapse", "1.70.0").mount().await; + + let server_info = client.server_vendor_info().await.unwrap(); + + assert_eq!(server_info.server_name, "Synapse"); + assert_eq!(server_info.version, "1.70.0"); +} + +#[async_test] +async fn test_server_vendor_info_with_missing_fields() { + use matrix_sdk::test_utils::mocks::MatrixMockServer; + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + // Mock the federation version endpoint with missing fields + server.mock_federation_version().ok_empty().mount().await; + + let server_info = client.server_vendor_info().await.unwrap(); + + // Should use defaults for missing fields + assert_eq!(server_info.server_name, "unknown"); + assert_eq!(server_info.version, "unknown"); +}