feat: Add support for MSC4171

Introduce support for MSC4171, enabling the designation of certain users
as service members. These flagged users are excluded from the room
display name calculation.

MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4171
This commit is contained in:
Damir Jelić
2024-11-21 13:22:53 +01:00
parent ee30008f38
commit 6501a44e6a
5 changed files with 214 additions and 23 deletions
Generated
+9 -9
View File
@@ -4753,7 +4753,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",
@@ -4770,7 +4770,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",
@@ -4793,7 +4793,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",
@@ -4825,7 +4825,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",
@@ -4850,7 +4850,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",
@@ -4864,7 +4864,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",
@@ -4876,7 +4876,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",
@@ -4885,7 +4885,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",
@@ -4901,7 +4901,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",
+4 -3
View File
@@ -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"
+8
View File
@@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
### Features
- Introduced support for
[MSC4171](https://github.com/matrix-org/matrix-rust-sdk/pull/4335), enabling
the designation of certain users as service members. These flagged users are
excluded from the room display name calculation.
([#4335](https://github.com/matrix-org/matrix-rust-sdk/pull/4335))
### Bug Fixes
- Fix an off-by-one error in the `ObservableMap` when the `remove()` method is
+160 -10
View File
@@ -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<Vec<String>> {
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?;
for hero in heroes {
// Construct a filter that is specific to this own user id, set of member hints,
// and accepts a `RoomHero` type.
let heroes_filter = heroes_filter(own_user_id, &member_hints);
let heroes_filter = |hero: &&RoomHero| heroes_filter(&hero.user_id);
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,30 @@ impl Room {
///
/// Returns a `(heroes_names, num_joined_invited)` tuple.
async fn compute_summary(&self) -> StoreResult<(Vec<String>, 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 = heroes_filter(&self.own_user_id, &member_hints);
let heroes_filter = |u: &RoomMember| heroes_filter(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;
//
// Note: Subtracting here is fine because `num_service_members` is a subset of
// `members.len()` due to the above filter operation.
let num_joined_invited = members.len() - num_service_members;
if num_joined_invited == 0
|| (num_joined_invited == 1 && members[0].user_id() == self.own_user_id)
@@ -729,12 +767,34 @@ 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();
Ok((heroes, num_joined_invited))
trace!(
?heroes,
num_joined_invited,
num_service_members,
"Computed a room summary since we didn't receive one."
);
Ok((heroes, num_joined_invited as u64))
}
async fn get_member_hints(&self) -> StoreResult<MemberHintsEventContent> {
Ok(self
.store
.get_state_event_static::<MemberHintsEventContent>(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(SyncStateEvent::Original(e)) => e.content))
.unwrap_or_default())
}
/// Returns the cached computed display name, if available.
@@ -1854,6 +1914,7 @@ fn compute_display_name_from_heroes(
#[cfg(test)]
mod tests {
use std::{
collections::BTreeSet,
ops::{Not, Sub},
str::FromStr,
sync::Arc,
@@ -1894,6 +1955,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};
@@ -2546,6 +2608,53 @@ 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 f = EventFactory::new().room(room_id!("!test:localhost"));
let members = changes
.state
.entry(room_id.to_owned())
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
let member_hints_content =
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
changes
.state
.entry(room_id.to_owned())
.or_default()
.entry(StateEventType::MemberHints)
.or_default()
.insert("".to_owned(), member_hints_content);
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);
@@ -2573,6 +2682,47 @@ 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 f = EventFactory::new().room(room_id!("!test:localhost"));
let members = changes
.state
.entry(room_id.to_owned())
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
let member_hints_content =
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
changes
.state
.entry(room_id.to_owned())
.or_default()
.entry(StateEventType::MemberHints)
.or_default()
.insert("".to_owned(), member_hints_content);
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);
+33 -1
View File
@@ -14,7 +14,10 @@
#![allow(missing_docs)]
use std::sync::atomic::{AtomicU64, Ordering::SeqCst};
use std::{
collections::BTreeSet,
sync::atomic::{AtomicU64, Ordering::SeqCst},
};
use as_variant::as_variant;
use matrix_sdk_common::deserialized_responses::{
@@ -22,6 +25,7 @@ use matrix_sdk_common::deserialized_responses::{
};
use ruma::{
events::{
member_hints::MemberHintsEventContent,
message::TextContentBlock,
poll::{
end::PollEndEventContent,
@@ -399,6 +403,34 @@ impl EventFactory {
event
}
/// Create a new `m.member_hints` event with the given service members.
///
/// ```
/// use std::collections::BTreeSet;
///
/// use matrix_sdk_test::event_factory::EventFactory;
/// use ruma::{
/// events::{member_hints::MemberHintsEventContent, SyncStateEvent},
/// owned_user_id, room_id,
/// serde::Raw,
/// user_id,
/// };
///
/// let factory = EventFactory::new().room(room_id!("!test:localhost"));
///
/// let event: Raw<SyncStateEvent<MemberHintsEventContent>> = factory
/// .member_hints(BTreeSet::from([owned_user_id!("@alice:localhost")]))
/// .sender(user_id!("@alice:localhost"))
/// .into_raw();
/// ```
pub fn member_hints(
&self,
service_members: BTreeSet<OwnedUserId>,
) -> EventBuilder<MemberHintsEventContent> {
// The `m.member_hints` event always has an empty state key, so let's set it.
self.event(MemberHintsEventContent::new(service_members)).state_key("")
}
/// Create a new plain/html `m.room.message`.
pub fn text_html(
&self,