sdk: Cache the client well-known file and add Client::rtc_foci which uses it.

This commit is contained in:
Doug
2025-06-04 14:04:29 +01:00
committed by Damir Jelić
parent c74295c604
commit ea28234d95
12 changed files with 226 additions and 49 deletions
Generated
+8 -16
View File
@@ -4449,8 +4449,7 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d910a9b75cbf0e88f74295997c1a41c3ab7a117879a029c72db815192c167a0d"
source = "git+https://github.com/ruma/ruma?rev=d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a#d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a"
dependencies = [
"assign",
"js_int",
@@ -4466,8 +4465,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc4ff88a70a3d1e7a2c5b51cca7499cb889b42687608ab664b9a216c49314d"
source = "git+https://github.com/ruma/ruma?rev=d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a#d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a"
dependencies = [
"as_variant",
"assign",
@@ -4490,8 +4488,7 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b75da013b362664c3e161662902e5da3f77e990525681b59c6035bac27e87b4"
source = "git+https://github.com/ruma/ruma?rev=d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a#d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a"
dependencies = [
"as_variant",
"base64",
@@ -4523,8 +4520,7 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.30.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ab3d1b54c32a65194ecc44bc7f7575df50ef4255b139547d7dcc1753dc883d"
source = "git+https://github.com/ruma/ruma?rev=d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a#d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a"
dependencies = [
"as_variant",
"indexmap",
@@ -4549,8 +4545,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373bc5a30b84574dfce3e75c33d79d6ba9843bf0eee1bf351f904eef9bea001a"
source = "git+https://github.com/ruma/ruma?rev=d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a#d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a"
dependencies = [
"http",
"js_int",
@@ -4564,8 +4559,7 @@ dependencies = [
[[package]]
name = "ruma-html"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "865afa2321e34fa836ea4c1d77ce0c2bb40f7d13fe18ee3e795091fd8d173a1d"
source = "git+https://github.com/ruma/ruma?rev=d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a#d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a"
dependencies = [
"as_variant",
"html5ever",
@@ -4577,8 +4571,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ad674b5e5368c53a2c90fde7dac7e30747004aaf7b1827b72874a25fc06d4d8"
source = "git+https://github.com/ruma/ruma?rev=d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a#d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a"
dependencies = [
"js_int",
"thiserror 2.0.11",
@@ -4587,8 +4580,7 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1182e83ee5cd10121974f163337b16af68a93eedfc7cdbdbd52307ac7e1d743"
source = "git+https://github.com/ruma/ruma?rev=d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a#d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a"
dependencies = [
"cfg-if",
"proc-macro-crate",
+4 -3
View File
@@ -60,7 +60,7 @@ reqwest = { version = "0.12.12", default-features = false }
rmp-serde = "1.3.0"
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
# branch until a proper release with breaking changes happens.
ruma = { version = "0.12.3", features = [
ruma = { git = "https://github.com/ruma/ruma", rev = "d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
@@ -73,11 +73,12 @@ ruma = { version = "0.12.3", features = [
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
"unstable-msc4143",
"unstable-msc4171",
"unstable-msc4278",
"unstable-msc4286",
] }
ruma-common = "0.15.2"
] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a" }
sentry = "0.36.0"
sentry-tracing = "0.36.0"
serde = { version = "1.0.217", features = ["rc"] }
+3
View File
@@ -52,6 +52,9 @@ No notable changes in this release.
([#4897](https://github.com/matrix-org/matrix-rust-sdk/pull/4897))
- [**breaking**] `RoomInfo::prev_state` has been removed due to being useless.
([#5054](https://github.com/matrix-org/matrix-rust-sdk/pull/5054))
- The cached `ServerCapabilities` has been renamed to `ServerInfo` and additionally contains
the well-known response alongside the existing server versions. Despite the old name, it
does not contain the server capabilities (yet?!).
## [0.10.0] - 2025-02-04
@@ -7,7 +7,10 @@ use assert_matches2::assert_let;
use growable_bloom_filter::GrowableBloomBuilder;
use matrix_sdk_test::{event_factory::EventFactory, test_json};
use ruma::{
api::MatrixVersion,
api::{
client::discovery::discover_homeserver::{HomeserverInfo, RtcFocusInfo},
MatrixVersion,
},
event_id,
events::{
presence::PresenceEvent,
@@ -34,7 +37,7 @@ use serde_json::{json, value::Value as JsonValue};
use super::{
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
RoomLoadSettings, ServerInfo,
RoomLoadSettings, ServerInfo, WellKnownResponse,
};
use crate::{
deserialized_responses::MemberEvent,
@@ -477,6 +480,12 @@ impl StateStoreIntegrationTests for DynStateStore {
let server_info = ServerInfo::new(
versions.iter().map(|version| version.to_string()).collect(),
[("org.matrix.experimental".to_owned(), true)].into(),
Some(WellKnownResponse {
homeserver: HomeserverInfo::new("matrix.example.com".to_owned()),
identity_server: None,
tile_server: None,
rtc_foci: vec![RtcFocusInfo::livekit("livekit.example.com".to_owned())],
}),
);
self.set_kv_data(
+1 -1
View File
@@ -82,7 +82,7 @@ pub use self::{
},
traits::{
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerInfo, StateStore,
StateStoreDataKey, StateStoreDataValue, StateStoreExt,
StateStoreDataKey, StateStoreDataValue, StateStoreExt, WellKnownResponse,
},
};
+48 -3
View File
@@ -24,7 +24,16 @@ use async_trait::async_trait;
use growable_bloom_filter::GrowableBloom;
use matrix_sdk_common::AsyncTraitDeps;
use ruma::{
api::{client::discovery::get_supported_versions, MatrixVersion},
api::{
client::discovery::{
discover_homeserver,
discover_homeserver::{
HomeserverInfo, IdentityServerInfo, RtcFocusInfo, TileServerInfo,
},
get_supported_versions,
},
MatrixVersion,
},
events::{
presence::PresenceEvent,
receipt::{Receipt, ReceiptThread, ReceiptType},
@@ -960,6 +969,10 @@ pub struct ServerInfo {
/// List of unstable features and their enablement status.
pub unstable_features: BTreeMap<String, bool>,
/// Information about the server found in the client well-known file.
#[serde(skip_serializing_if = "Option::is_none")]
pub well_known: Option<WellKnownResponse>,
/// Last time we fetched this data from the server, in milliseconds since
/// epoch.
last_fetch_ts: f64,
@@ -970,8 +983,12 @@ impl ServerInfo {
pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24 * 7) as _; // seven days
/// Encode server info into this serializable struct.
pub fn new(versions: Vec<String>, unstable_features: BTreeMap<String, bool>) -> Self {
Self { versions, unstable_features, last_fetch_ts: now_timestamp_ms() }
pub fn new(
versions: Vec<String>,
unstable_features: BTreeMap<String, bool>,
well_known: Option<WellKnownResponse>,
) -> Self {
Self { versions, unstable_features, well_known, last_fetch_ts: now_timestamp_ms() }
}
/// Decode server info from this serializable struct.
@@ -998,6 +1015,33 @@ impl ServerInfo {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
/// A serialisable representation of discover_homeserver::Response.
pub struct WellKnownResponse {
/// Information about the homeserver to connect to.
pub homeserver: HomeserverInfo,
/// Information about the identity server to connect to.
pub identity_server: Option<IdentityServerInfo>,
/// Information about the tile server to use to display location data.
pub tile_server: Option<TileServerInfo>,
/// A list of the available MatrixRTC foci, ordered by priority.
pub rtc_foci: Vec<RtcFocusInfo>,
}
impl From<discover_homeserver::Response> for WellKnownResponse {
fn from(response: discover_homeserver::Response) -> Self {
Self {
homeserver: response.homeserver,
identity_server: response.identity_server,
tile_server: response.tile_server,
rtc_foci: response.rtc_foci,
}
}
}
/// Get the current timestamp as the number of milliseconds since Unix Epoch.
fn now_timestamp_ms() -> f64 {
SystemTime::now()
@@ -1180,6 +1224,7 @@ mod tests {
let mut server_info = ServerInfo {
versions: Default::default(),
unstable_features: Default::default(),
well_known: Default::default(),
last_fetch_ts: now_timestamp_ms() - ServerInfo::STALE_THRESHOLD - 1.0,
};
+3
View File
@@ -78,6 +78,9 @@ All notable changes to this project will be documented in this file.
([#5047](https://github.com/matrix-org/matrix-rust-sdk/pull/5047))
- `Room::set_unread_flag()` is now a no-op if the unread flag already has the wanted value.
([#5055](https://github.com/matrix-org/matrix-rust-sdk/pull/5055))
- `ClientServerCapabilities` has been renamed to `ClientServerInfo`. Alongside this,
`Client::reset_server_info` is now `Client::reset_server_info` and `Client::fetch_server_capabilities`
is now `Client::fetch_server_versions`, returning the server versions response directly.
## [0.11.0] - 2025-04-11
@@ -516,7 +516,7 @@ async fn test_insecure_clients() -> anyhow::Result<()> {
let server = MatrixMockServer::new().await;
let server_url = server.server().uri();
server.mock_well_known().ok().expect(1).named("well_known").mount().await;
server.mock_well_known().ok().expect(1..).named("well_known").mount().await;
server.mock_versions().ok().expect(1..).named("versions").mount().await;
let oauth_server = server.oauth();
@@ -53,6 +53,7 @@ pub(super) struct HomeserverDiscoveryResult {
pub server: Option<Url>,
pub homeserver: Url,
pub supported_versions: Option<get_supported_versions::Response>,
pub well_known: Option<discover_homeserver::Response>,
}
impl HomeserverConfig {
@@ -68,6 +69,7 @@ impl HomeserverConfig {
server: None, // We can't know the `server` if we only have a `homeserver`.
homeserver,
supported_versions: None,
well_known: None,
}
}
@@ -79,18 +81,19 @@ impl HomeserverConfig {
server: Some(server),
homeserver: Url::parse(&well_known.homeserver.base_url)?,
supported_versions: None,
well_known: Some(well_known),
}
}
Self::ServerNameOrHomeserverUrl(server_name_or_url) => {
let (server, homeserver, supported_versions) =
let (server, homeserver, supported_versions, well_known) =
discover_homeserver_from_server_name_or_url(
server_name_or_url.to_owned(),
http_client,
)
.await?;
HomeserverDiscoveryResult { server, homeserver, supported_versions }
HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known }
}
})
}
@@ -102,7 +105,15 @@ impl HomeserverConfig {
async fn discover_homeserver_from_server_name_or_url(
mut server_name_or_url: String,
http_client: &HttpClient,
) -> Result<(Option<Url>, Url, Option<get_supported_versions::Response>), ClientBuildError> {
) -> Result<
(
Option<Url>,
Url,
Option<get_supported_versions::Response>,
Option<discover_homeserver::Response>,
),
ClientBuildError,
> {
let mut discovery_error: Option<ClientBuildError> = None;
// Attempt discovery as a server name first.
@@ -117,7 +128,12 @@ async fn discover_homeserver_from_server_name_or_url(
match discover_homeserver(server_name, &protocol, http_client).await {
Ok((server, well_known)) => {
return Ok((Some(server), Url::parse(&well_known.homeserver.base_url)?, None));
return Ok((
Some(server),
Url::parse(&well_known.homeserver.base_url)?,
None,
Some(well_known),
));
}
Err(e) => {
debug!(error = %e, "Well-known discovery failed.");
@@ -138,7 +154,7 @@ async fn discover_homeserver_from_server_name_or_url(
// Make sure the URL is definitely for a homeserver.
match get_supported_versions(&homeserver_url, http_client).await {
Ok(response) => {
return Ok((None, homeserver_url, Some(response)));
return Ok((None, homeserver_url, Some(response), None));
}
Err(e) => {
debug!(error = %e, "Checking supported versions failed.");
+6 -3
View File
@@ -525,7 +525,7 @@ impl ClientBuilder {
let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
#[allow(unused_variables)]
let HomeserverDiscoveryResult { server, homeserver, supported_versions } =
let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
homeserver_cfg.discover(&http_client).await?;
let sliding_sync_version = {
@@ -560,8 +560,11 @@ impl ClientBuilder {
// Enable the send queue by default.
let send_queue = Arc::new(SendQueueData::new(true));
let server_info =
ClientServerInfo { server_versions: self.server_versions, unstable_features: None };
let server_info = ClientServerInfo {
server_versions: self.server_versions,
unstable_features: None,
well_known: well_known.map(Into::into),
};
let event_cache = OnceCell::new();
let inner = ClientInner::new(
+113 -14
View File
@@ -31,7 +31,7 @@ use futures_util::StreamExt;
use matrix_sdk_base::crypto::store::LockableCryptoStore;
use matrix_sdk_base::{
event_cache::store::EventCacheStoreLock,
store::{DynStateStore, RoomLoadSettings, ServerInfo},
store::{DynStateStore, RoomLoadSettings, ServerInfo, WellKnownResponse},
sync::{Notification, RoomUpdates},
BaseClient, RoomInfoNotableUpdate, RoomState, RoomStateFilter, SendOutsideWasm, SessionMeta,
StateStoreDataKey, StateStoreDataValue, SyncOutsideWasm,
@@ -47,6 +47,8 @@ use ruma::{
device::{delete_devices, get_devices, update_device},
directory::{get_public_rooms, get_public_rooms_filtered},
discovery::{
discover_homeserver,
discover_homeserver::RtcFocusInfo,
get_capabilities::{self, Capabilities},
get_supported_versions,
},
@@ -1740,8 +1742,8 @@ impl Client {
pub async fn fetch_server_versions(
&self,
request_config: Option<RequestConfig>,
) -> HttpResult<(Vec<String>, BTreeMap<String, bool>)> {
let resp = self
) -> HttpResult<get_supported_versions::Response> {
let server_versions = self
.inner
.http_client
.send(
@@ -1754,7 +1756,42 @@ impl Client {
)
.await?;
Ok((resp.versions, resp.unstable_features))
Ok(server_versions)
}
/// Fetches client well_known from network; no caching.
pub async fn fetch_client_well_known(&self) -> Option<discover_homeserver::Response> {
let server_url_string = self
.server()
.unwrap_or(
// Sometimes people configure their well-known directly on the homeserver so use
// this as a fallback when the server name is unknown.
&self.homeserver(),
)
.to_string();
let well_known = self
.inner
.http_client
.send(
discover_homeserver::Request::new(),
Some(RequestConfig::short_retry()),
server_url_string,
None,
&[MatrixVersion::V1_0],
Default::default(),
)
.await;
match well_known {
Ok(well_known) => Some(well_known),
Err(http_error) => {
// It is perfectly valid to not have a well-known file.
// Maybe we should check for a specific error code to be sure?
warn!("Failed to fetch client well-known: {http_error}");
None
}
}
}
/// Load server info from storage, or fetch them from network and cache
@@ -1777,8 +1814,13 @@ impl Client {
}
}
let (versions, unstable_features) = self.fetch_server_versions(None).await?;
let server_info = ServerInfo::new(versions.clone(), unstable_features.clone());
let server_versions = self.fetch_server_versions(None).await?;
let well_known = self.fetch_client_well_known().await;
let server_info = ServerInfo::new(
server_versions.versions.clone(),
server_versions.unstable_features.clone(),
well_known.map(Into::into),
);
// Attempt to cache the result in storage.
{
@@ -1821,6 +1863,7 @@ impl Client {
guard.server_versions = Some(versions.into());
guard.unstable_features = Some(server_info.unstable_features);
guard.well_known = server_info.well_known;
// SAFETY: both fields were set above, so the function will always return some.
Ok(f(&guard).unwrap())
@@ -1871,6 +1914,36 @@ impl Client {
.await
}
/// Get information about the homeserver's advertised RTC foci by fetching
/// the well-known file from the server or the cache.
///
/// # Examples
/// ```no_run
/// # use matrix_sdk::{Client, config::SyncSettings, ruma::api::client::discovery::discover_homeserver::RtcFocusInfo};
/// # use url::Url;
/// # async {
/// # let homeserver = Url::parse("http://localhost:8080")?;
/// # let mut client = Client::new(homeserver).await?;
/// let rtc_foci = client.rtc_foci().await?;
/// let default_livekit_focus_info = rtc_foci.iter().find_map(|focus| match focus {
/// RtcFocusInfo::LiveKit(info) => Some(info),
/// _ => None,
/// });
/// if let Some(info) = default_livekit_focus_info {
/// println!("Default LiveKit service URL: {}", info.service_url);
/// }
/// # anyhow::Ok(()) };
/// ```
pub async fn rtc_foci(&self) -> HttpResult<Vec<RtcFocusInfo>> {
self.get_or_load_and_cache_server_info(|server_info| {
server_info
.well_known
.as_ref()
.and_then(|well_known| well_known.rtc_foci.clone().into())
})
.await
}
/// Empty the server version and unstable features cache.
///
/// Since the SDK caches server info (versions, unstable features,
@@ -2625,6 +2698,8 @@ struct ClientServerInfo {
/// The unstable features and their on/off state on the server.
unstable_features: Option<BTreeMap<String, bool>>,
well_known: Option<WellKnownResponse>,
}
// The http mocking library is not supported for wasm32
@@ -2649,7 +2724,13 @@ pub(crate) mod tests {
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use ruma::{
api::{client::room::create_room::v3::Request as CreateRoomRequest, MatrixVersion},
api::{
client::{
discovery::discover_homeserver::RtcFocusInfo,
room::create_room::v3::Request as CreateRoomRequest,
},
MatrixVersion,
},
assign,
events::{
ignored_user_list::IgnoredUserListEventContent,
@@ -3046,16 +3127,17 @@ pub(crate) mod tests {
let server_url = server.uri();
let domain = server_url.strip_prefix("http://").unwrap();
let server_name = <&ServerName>::try_from(domain).unwrap();
let rtc_foci = vec![RtcFocusInfo::livekit("https://livekit.example.com".to_owned())];
Mock::given(method("GET"))
let well_known_mock = Mock::given(method("GET"))
.and(path("/.well-known/matrix/client"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
test_json::WELL_KNOWN.to_string().replace("HOMESERVER_URL", server_url.as_ref()),
"application/json",
))
.named("well known mock")
.expect(2)
.mount(&server)
.expect(2) // One for ClientBuilder discovery, one for the ServerInfo cache.
.mount_as_scoped(&server)
.await;
let versions_mock = Mock::given(method("GET"))
@@ -3079,13 +3161,14 @@ pub(crate) mod tests {
assert_eq!(client.server_versions().await.unwrap().len(), 1);
// This second call hits the in-memory cache.
// These subsequent calls hit the in-memory cache.
assert!(client.server_versions().await.unwrap().contains(&MatrixVersion::V1_0));
assert_eq!(client.rtc_foci().await.unwrap(), rtc_foci);
drop(client);
let client = Client::builder()
.insecure_server_name_no_tls(server_name)
.homeserver_url(server.uri()) // Configure this client directly so as to not hit the discovery endpoint.
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(memory_store.clone()),
@@ -3094,18 +3177,33 @@ pub(crate) mod tests {
.await
.unwrap();
// This third call hits the on-disk cache.
// This call to the new client hits the on-disk cache.
assert_eq!(
client.unstable_features().await.unwrap().get("org.matrix.e2e_cross_signing"),
Some(&true)
);
// Then this call hits the in-memory cache.
assert_eq!(client.rtc_foci().await.unwrap(), rtc_foci);
drop(versions_mock);
drop(well_known_mock);
server.verify().await;
// Now, reset the cache, and observe the endpoint being called again once.
// Now, reset the cache, and observe the endpoints being called again once.
client.reset_server_info().await.unwrap();
Mock::given(method("GET"))
.and(path("/.well-known/matrix/client"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
test_json::WELL_KNOWN.to_string().replace("HOMESERVER_URL", server_url.as_ref()),
"application/json",
))
.named("second well known mock")
.expect(1)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/_matrix/client/versions"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
@@ -3118,6 +3216,7 @@ pub(crate) mod tests {
assert_eq!(client.server_versions().await.unwrap().len(), 1);
// Hits in-memory cache again.
assert!(client.server_versions().await.unwrap().contains(&MatrixVersion::V1_0));
assert_eq!(client.rtc_foci().await.unwrap(), rtc_foci);
}
#[async_test]
@@ -359,7 +359,13 @@ pub static WELL_KNOWN: Lazy<JsonValue> = Lazy::new(|| {
json!({
"m.homeserver": {
"base_url": "HOMESERVER_URL"
}
},
"m.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://livekit.example.com",
}
]
})
});