diff --git a/Cargo.lock b/Cargo.lock index 85e141862ab..f1ad0d9a3c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4722,7 +4722,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.11.1" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "assign", "js_int", @@ -4739,7 +4739,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.19.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "as_variant", "assign", @@ -4762,7 +4762,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.14.1" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "as_variant", "base64 0.22.1", @@ -4794,7 +4794,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.29.1" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -4819,7 +4819,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.10.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "http", "js_int", @@ -4833,7 +4833,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.3.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "as_variant", "html5ever", @@ -4845,7 +4845,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "js_int", "thiserror 2.0.3", @@ -4854,7 +4854,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.14.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "cfg-if", "once_cell", @@ -4870,7 +4870,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.10.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index 0b2fb061b6d..79ae1c835cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ default-members = ["benchmarks", "crates/*", "labs/*"] resolver = "2" [workspace.package] -rust-version = "1.80" +rust-version = "1.82" [workspace.dependencies] anyhow = "1.0.93" @@ -56,7 +56,7 @@ proptest = { version = "1.5.0", default-features = false, features = ["std"] } rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } rmp-serde = "1.3.0" -ruma = { git = "https://github.com/ruma/ruma.git", rev = "35fda7f2f7811156df2e60b223dbf136fc143bc8", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "c91499fc464adc865a7c99d0ce0b35982ad96711", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -69,8 +69,9 @@ ruma = { git = "https://github.com/ruma/ruma.git", rev = "35fda7f2f7811156df2e60 "unstable-msc3489", "unstable-msc4075", "unstable-msc4140", + "unstable-msc4171", ] } -ruma-common = { git = "https://github.com/ruma/ruma.git", rev = "35fda7f2f7811156df2e60b223dbf136fc143bc8" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "c91499fc464adc865a7c99d0ce0b35982ad96711" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 9af22f33fab..201a78d797c 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -20,6 +20,7 @@ use std::{ sync::{atomic::AtomicBool, Arc}, }; +use as_variant::as_variant; use bitflags::bitflags; use eyeball::{SharedObservable, Subscriber}; use futures_util::{Stream, StreamExt}; @@ -35,6 +36,7 @@ use ruma::{ call::member::{CallMemberStateKey, MembershipData}, direct::OwnedDirectUserIdentifier, ignored_user_list::IgnoredUserListEventContent, + member_hints::MemberHintsEventContent, receipt::{Receipt, ReceiptThread, ReceiptType}, room::{ avatar::{self, RoomAvatarEventContent}, @@ -50,7 +52,7 @@ use ruma::{ }, tag::{TagEventContent, Tags}, AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, - RoomAccountDataEventType, + RoomAccountDataEventType, SyncStateEvent, }, room::RoomType, serde::Raw, @@ -59,7 +61,7 @@ use ruma::{ }; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; -use tracing::{debug, field::debug, info, instrument, warn}; +use tracing::{debug, field::debug, info, instrument, trace, warn}; use super::{ members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName, @@ -68,7 +70,9 @@ use super::{ #[cfg(feature = "experimental-sliding-sync")] use crate::latest_event::LatestEvent; use crate::{ - deserialized_responses::{DisplayName, MemberEvent, RawSyncOrStrippedState}, + deserialized_responses::{ + DisplayName, MemberEvent, RawSyncOrStrippedState, SyncOrStrippedState, + }, notification_settings::RoomNotificationMode, read_receipts::RoomReadReceipts, store::{DynStateStore, Result as StoreResult, StateStoreExt}, @@ -222,6 +226,18 @@ impl From<&MembershipState> for RoomState { /// try to behave similarly here. const NUM_HEROES: usize = 5; +/// A filter to remove our own user and the users specified in the member hints +/// state event, so called service members, from the list of heroes. +/// +/// The heroes will then be used to calculate a display name for the room if one +/// wasn't explicitly defined. +fn heroes_filter<'a>( + own_user_id: &'a UserId, + member_hints: &'a MemberHintsEventContent, +) -> impl Fn(&UserId) -> bool + use<'a> { + move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id) +} + impl Room { /// The size of the latest_encrypted_events RingBuffer // SAFETY: `new_unchecked` is safe because 10 is not zero. @@ -678,12 +694,16 @@ impl Room { /// /// Returns the display names as a list of strings. async fn extract_heroes(&self, heroes: &[RoomHero]) -> StoreResult> { - let own_user_id = self.own_user_id().as_str(); - let mut names = Vec::with_capacity(heroes.len()); - let heroes = heroes.iter().filter(|hero| hero.user_id != own_user_id); + let own_user_id = self.own_user_id(); + let member_hints = self.get_member_hints().await?; + + // Construct a filter that is specific to this own user id, set of member hints, + // and accepts a `RoomHero` type. + let heroes_filter = + |hero: &&RoomHero| heroes_filter(own_user_id, &member_hints)(&hero.user_id); - for hero in heroes { + for hero in heroes.iter().filter(heroes_filter) { if let Some(display_name) = &hero.display_name { names.push(display_name.clone()); } else { @@ -710,12 +730,27 @@ impl Room { /// /// Returns a `(heroes_names, num_joined_invited)` tuple. async fn compute_summary(&self) -> StoreResult<(Vec, u64)> { + let member_hints = self.get_member_hints().await?; + + // Construct a filter that is specific to this own user id, set of member hints, + // and accepts a `RoomMember` type. + let heroes_filter = + |u: &RoomMember| heroes_filter(self.own_user_id(), &member_hints)(u.user_id()); + let mut members = self.members(RoomMemberships::JOIN | RoomMemberships::INVITE).await?; + // If we have some service members, they shouldn't count to the number of + // joined/invited members, otherwise we'll wrongly assume that there are more + // members in the room than they are for the "Bob and 2 others" case. + let num_service_members = members + .iter() + .filter(|member| member_hints.service_members.contains(member.user_id())) + .count(); + // We can make a good prediction of the total number of joined and invited // members here. This might be incorrect if the database info is // outdated. - let num_joined_invited = members.len() as u64; + let num_joined_invited = members.len().saturating_sub(num_service_members) as u64; if num_joined_invited == 0 || (num_joined_invited == 1 && members[0].user_id() == self.own_user_id) @@ -729,14 +764,38 @@ impl Room { let heroes = members .into_iter() - .filter(|u| u.user_id() != self.own_user_id) + .filter(heroes_filter) .take(NUM_HEROES) .map(|u| u.name().to_owned()) .collect(); + trace!( + ?heroes, + num_joined_invited, + num_service_members, + "Computed a room summary since we didn't receive one." + ); + Ok((heroes, num_joined_invited)) } + async fn get_member_hints(&self) -> StoreResult { + Ok(self + .store + .get_state_event_static::(self.room_id()) + .await? + .and_then(|event| { + event + .deserialize() + .inspect_err(|e| warn!("Couldn't deserialize the member hints event: {e}")) + .ok() + }) + .and_then(|event| as_variant!(event, SyncOrStrippedState::Sync)) + .and_then(|event| as_variant!(event, SyncStateEvent::Original)) + .map(|e| e.content) + .unwrap_or_default()) + } + /// Returns the cached computed display name, if available. /// /// This cache is refilled every time we call @@ -1854,6 +1913,7 @@ fn compute_display_name_from_heroes( #[cfg(test)] mod tests { use std::{ + collections::BTreeSet, ops::{Not, Sub}, str::FromStr, sync::Arc, @@ -1877,6 +1937,7 @@ mod tests { CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData, LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent, }, + member_hints::{MemberHintsEventContent, SyncMemberHintsEvent}, room::{ canonical_alias::RoomCanonicalAliasEventContent, encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent}, @@ -1896,6 +1957,7 @@ mod tests { OwnedEventId, OwnedUserId, UserId, }; use serde_json::json; + use similar_asserts::assert_eq; use stream_assert::{assert_pending, assert_ready}; use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo}; @@ -2385,6 +2447,23 @@ mod tests { Raw::new(&ev_json).unwrap().cast() } + fn make_member_hints_event( + service_members: BTreeSet, + ) -> Raw { + let content = assign!(MemberHintsEventContent::default(), { service_members }); + + let ev_json = json!({ + "type": "m.room.member", + "content":content, + "sender": "@somebody:localhost", + "state_key": "", + "event_id": "$h29iv0s1:example.com", + "origin_server_ts": 208, + }); + + Raw::new(&ev_json).unwrap().cast() + } + #[async_test] async fn test_display_name_for_joined_room_is_empty_if_no_info() { let (_, room) = make_room_test_helper(RoomState::Joined); @@ -2560,6 +2639,50 @@ mod tests { ); } + #[async_test] + async fn test_display_name_dm_joined_service_members() { + let (store, room) = make_room_test_helper(RoomState::Joined); + let room_id = room_id!("!test:localhost"); + + let matthew = user_id!("@sahasrhala:example.org"); + let me = user_id!("@me:example.org"); + let bot = user_id!("@bot:example.org"); + + let mut changes = StateChanges::new("".to_owned()); + let summary = assign!(RumaSummary::new(), { + joined_member_count: Some(2u32.into()), + heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()], + }); + + let members = changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::RoomMember) + .or_default(); + members.insert(matthew.into(), make_member_event(matthew, "Matthew").cast()); + members.insert(bot.into(), make_member_event(bot, "Bot").cast()); + members.insert(me.into(), make_member_event(me, "Me").cast()); + + let member_hints_content = make_member_hints_event(BTreeSet::from([bot.to_owned()])); + changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::MemberHints) + .or_default() + .insert("".to_owned(), member_hints_content.cast()); + + store.save_changes(&changes).await.unwrap(); + + room.inner.update_if(|info| info.update_from_ruma_summary(&summary)); + // Bot should not contribute to the display name. + assert_eq!( + room.compute_display_name().await.unwrap(), + RoomDisplayName::Calculated("Matthew".to_owned()) + ); + } + #[async_test] async fn test_display_name_dm_joined_no_heroes() { let (store, room) = make_room_test_helper(RoomState::Joined); @@ -2585,6 +2708,44 @@ mod tests { ); } + #[async_test] + async fn test_display_name_dm_joined_no_heroes_service_members() { + let (store, room) = make_room_test_helper(RoomState::Joined); + let room_id = room_id!("!test:localhost"); + + let matthew = user_id!("@matthew:example.org"); + let me = user_id!("@me:example.org"); + let bot = user_id!("@bot:example.org"); + + let mut changes = StateChanges::new("".to_owned()); + + let members = changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::RoomMember) + .or_default(); + members.insert(matthew.into(), make_member_event(matthew, "Matthew").cast()); + members.insert(me.into(), make_member_event(me, "Me").cast()); + members.insert(bot.into(), make_member_event(bot, "Bot").cast()); + + let member_hints_content = make_member_hints_event(BTreeSet::from([bot.to_owned()])); + changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::MemberHints) + .or_default() + .insert("".to_owned(), member_hints_content.cast()); + + store.save_changes(&changes).await.unwrap(); + + assert_eq!( + room.compute_display_name().await.unwrap(), + RoomDisplayName::Calculated("Matthew".to_owned()) + ); + } + #[async_test] async fn test_display_name_deterministic() { let (store, room) = make_room_test_helper(RoomState::Joined);