Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bindings/matrix-sdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ crate-type = [
]

[features]
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis", "experimental-push-secrets", "experimental-search"]
default = ["bundled-sqlite", "unstable-msc4274", "unstable-msc4383", "experimental-element-recent-emojis", "experimental-push-secrets", "experimental-search"]
experimental-search = ["matrix-sdk/experimental-search"]
# Use SQLite for the session storage.
sqlite = ["matrix-sdk/sqlite"]
Expand All @@ -33,6 +33,7 @@ bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"]
# Use IndexedDB for the session storage.
indexeddb = ["matrix-sdk/indexeddb"]
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
unstable-msc4383 = ["matrix-sdk/unstable-msc4383"]
# Required when targeting a Javascript environment, like Wasm in a browser.
js = ["matrix-sdk-ui/js"]
# Enable sentry error monitoring, not compatible with Wasm platforms.
Expand Down
13 changes: 9 additions & 4 deletions bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2079,10 +2079,15 @@ impl Client {
&& self.inner.unstable_features().await?.contains(&ruma::api::FeatureFlag::Msc4108))
}

/// 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.
/// Get information about the homeserver implementation.
///
/// Reads the MSC4383 `server` object on `GET /_matrix/client/versions`
/// when available, and falls back to the federation
/// `/_matrix/federation/v1/version` endpoint for homeservers that have
/// not yet adopted MSC4383. Missing `name` or `version` fields are
/// reported as the literal string `"unknown"`; callers that need to
/// distinguish "advertised" from "not advertised" must compare against
/// this sentinel.
pub async fn server_vendor_info(&self) -> Result<matrix_sdk::ServerVendorInfo, ClientError> {
Ok(self.inner.server_vendor_info(None).await?)
}
Expand Down
7 changes: 7 additions & 0 deletions crates/matrix-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode", "feder
# Add support for inline media galleries via msgtypes
unstable-msc4274 = ["ruma/unstable-msc4274", "matrix-sdk-base/unstable-msc4274"]

# Read server implementation identification from the client-server `/versions`
# endpoint, per MSC4383. Decoupled from `federation-api` on purpose: a consumer
# that only wants the C-S read path can drop `federation-api` and skip the
# `ruma-federation-api` dependency entirely, at the cost of the legacy
# `/_matrix/federation/v1/version` fallback.
unstable-msc4383 = ["ruma/unstable-msc4383"]

experimental-search = ["matrix-sdk-search"]

experimental-element-recent-emojis = ["matrix-sdk-base/experimental-element-recent-emojis"]
Expand Down
7 changes: 7 additions & 0 deletions crates/matrix-sdk/changelog.d/+msc4383.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Read homeserver implementation identification from the client-server
`GET /_matrix/client/versions` response per [MSC4383], behind the new
`unstable-msc4383` Cargo feature. `Client::server_vendor_info` now prefers
the MSC4383 `server` object and falls back to `GET /_matrix/federation/v1/version`
for homeservers that have not yet adopted MSC4383.

[MSC4383]: https://github.com/matrix-org/matrix-spec-proposals/pull/4383
99 changes: 84 additions & 15 deletions crates/matrix-sdk/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,11 @@ pub enum SessionChange {
TokensRefreshed,
}

/// Information about the server vendor obtained from the federation API.
/// Information about the homeserver implementation.
///
/// Preferred source is the MSC4383 `server` object on
/// `GET /_matrix/client/versions`. When that object is absent (older servers),
/// the data falls back to `GET /_matrix/federation/v1/version`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct ServerVendorInfo {
Expand All @@ -180,6 +184,16 @@ pub struct ServerVendorInfo {
pub version: String,
}

impl ServerVendorInfo {
/// A `ServerVendorInfo` whose fields read `"unknown"`. Returned when no
/// source advertised the homeserver identification; see
/// [`Client::server_vendor_info`] for the dispatch policy.
#[cfg(all(feature = "unstable-msc4383", not(feature = "federation-api")))]
pub(crate) fn unknown() -> Self {
Self { server_name: "unknown".to_owned(), version: "unknown".to_owned() }
}
}

/// Information about a map tile server advertised by the homeserver through the
/// `tile_server` field of the matrix client well-known (MSC3488).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -588,10 +602,24 @@ impl Client {
HomeserverCapabilities::new(self.clone())
}

/// Get the server vendor information from the federation API.
/// Get information about the homeserver implementation.
///
/// This method calls the `/_matrix/federation/v1/version` endpoint to get
/// both the server's software name and version.
/// Under the `unstable-msc4383` feature this reads the `server` object on
/// `GET /_matrix/client/versions`
/// ([MSC4383](https://github.com/matrix-org/matrix-spec-proposals/pull/4383)).
/// When that object is absent (the homeserver has not adopted MSC4383)
/// and the `federation-api` feature is also enabled, the method falls
/// back to `GET /_matrix/federation/v1/version`. With only
/// `unstable-msc4383` enabled, an absent object yields a
/// `ServerVendorInfo` whose fields both read `"unknown"`.
///
/// Transport-layer errors on the chosen endpoint are propagated; only an
/// absent `server` object triggers the fallback. A present-but-empty
/// `server` object is treated as a successful response from a MSC4383
/// homeserver that has nothing to advertise and does not fall back to
/// federation. Missing `name` or `version` within an otherwise successful
/// response are reported as `"unknown"`. The same `request_config` is
/// forwarded to both endpoints when the federation fallback is taken.
///
/// # Examples
///
Expand All @@ -602,30 +630,71 @@ impl Client {
/// # let homeserver = Url::parse("http://example.com")?;
/// let client = Client::new(homeserver).await?;
///
/// let server_info = client.server_vendor_info(None).await?;
/// println!(
/// "Server: {}, Version: {}",
/// server_info.server_name, server_info.version
/// );
/// let info = client.server_vendor_info(None).await?;
/// if info.server_name == "unknown" {
/// println!("homeserver did not advertise its identification");
/// } else {
/// println!("homeserver is {} {}", info.server_name, info.version);
/// }
/// # anyhow::Ok(()) };
/// ```
#[cfg(feature = "federation-api")]
#[cfg(any(feature = "federation-api", feature = "unstable-msc4383"))]
pub async fn server_vendor_info(
&self,
request_config: Option<RequestConfig>,
) -> HttpResult<ServerVendorInfo> {
#[cfg(feature = "unstable-msc4383")]
if let Some(info) = self.read_msc4383_server_info(request_config).await? {
return Ok(info);
}

#[cfg(feature = "federation-api")]
{
return self.read_federation_server_info(request_config).await;
}

#[cfg(all(feature = "unstable-msc4383", not(feature = "federation-api")))]
Ok(ServerVendorInfo::unknown())
}

/// Read the MSC4383 `server` object from `GET /_matrix/client/versions`.
///
/// Returns `Ok(Some(_))` when the homeserver advertised the object,
/// `Ok(None)` when the object is absent, and propagates transport-layer
/// errors.
#[cfg(feature = "unstable-msc4383")]
async fn read_msc4383_server_info(
&self,
request_config: Option<RequestConfig>,
) -> HttpResult<Option<ServerVendorInfo>> {
let resp = self.fetch_server_versions_inner(false, request_config).await?;
Ok(resp.server.map(|server| ServerVendorInfo {
server_name: server.name.unwrap_or_else(|| "unknown".to_owned()),
version: server.version.unwrap_or_else(|| "unknown".to_owned()),
}))
}

/// Read homeserver identification from `GET
/// /_matrix/federation/v1/version`.
///
/// This is the pre-MSC4383 cross-interface path; see
/// [`Self::server_vendor_info`] for the dispatch policy.
#[cfg(feature = "federation-api")]
async fn read_federation_server_info(
&self,
request_config: Option<RequestConfig>,
) -> HttpResult<ServerVendorInfo> {
use ruma::api::federation::discovery::get_server_version;

let res = self
.send_inner(get_server_version::v1::Request::new(), request_config, Default::default())
.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 })
Ok(ServerVendorInfo {
server_name: server.name.unwrap_or_else(|| "unknown".to_owned()),
version: server.version.unwrap_or_else(|| "unknown".to_owned()),
})
}

/// Get a copy of the default request config.
Expand Down
49 changes: 45 additions & 4 deletions crates/matrix-sdk/src/test_utils/mocks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3549,6 +3549,7 @@ impl<'a> MockEndpoint<'a, BanUserEndpoint> {
pub struct VersionsEndpoint {
versions: Vec<&'static str>,
features: BTreeMap<&'static str, bool>,
server_info: Option<serde_json::Value>,
}

impl VersionsEndpoint {
Expand All @@ -3563,7 +3564,11 @@ impl VersionsEndpoint {

impl Default for VersionsEndpoint {
fn default() -> Self {
Self { versions: Self::commonly_supported_versions(), features: BTreeMap::new() }
Self {
versions: Self::commonly_supported_versions(),
features: BTreeMap::new(),
server_info: None,
}
}
}

Expand All @@ -3574,10 +3579,17 @@ impl<'a> MockEndpoint<'a, VersionsEndpoint> {
pub fn ok(mut self) -> MatrixMock<'a> {
let features = std::mem::take(&mut self.endpoint.features);
let versions = std::mem::take(&mut self.endpoint.versions);
self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
let server_info = self.endpoint.server_info.take();
let mut body = json!({
"unstable_features": features,
"versions": versions
})))
"versions": versions,
});
if let Some(info) = server_info
&& let Some(obj) = body.as_object_mut()
{
obj.insert("net.zemos.msc4383.server".to_owned(), info);
}
self.respond_with(ResponseTemplate::new(200).set_body_json(body))
}

/// Set the supported flag for the given unstable feature in the response of
Expand Down Expand Up @@ -3607,6 +3619,35 @@ impl<'a> MockEndpoint<'a, VersionsEndpoint> {
self.endpoint.versions = versions;
self
}

/// Populate the MSC4383 `server` object on the response.
///
/// The object is serialised under the unstable wire key
/// `net.zemos.msc4383.server`. When MSC4383 stabilises this helper must
/// also emit the stable `server` key alongside (or in place of) the
/// unstable one; matrix-sdk reads only the unstable key today. For
/// partial-field shapes (only `name` or only `version` advertised), use
/// [`Self::with_server_info_partial`].
pub fn with_server_info(self, name: &str, version: &str) -> Self {
self.with_server_info_partial(Some(name), Some(version))
}

/// Populate the MSC4383 `server` object with optionally-absent fields.
///
/// `None` for a field omits that key from the wire object. Passing
/// `(None, None)` still emits an empty `server` object, which exercises
/// the "object present but both fields missing" case.
pub fn with_server_info_partial(mut self, name: Option<&str>, version: Option<&str>) -> Self {
let mut obj = serde_json::Map::new();
if let Some(n) = name {
obj.insert("name".to_owned(), json!(n));
}
if let Some(v) = version {
obj.insert("version".to_owned(), json!(v));
}
self.endpoint.server_info = Some(serde_json::Value::Object(obj));
self
}
}

/// A prebuilt mock for the room summary endpoint.
Expand Down
Loading
Loading