1use std::{
16 collections::{BTreeSet, HashMap},
17 mem, slice,
18 sync::Arc,
19};
20
21use bitflags::bitflags;
22use futures_util::future;
23#[cfg(feature = "unstable-msc4426")]
24use ruma::profile::{Call, CallProfileField, Status, StatusProfileField, UserProfile};
25use ruma::{
26 Int, MxcUri, OwnedUserId, UserId,
27 events::{
28 MessageLikeEventType, StateEventType,
29 ignored_user_list::IgnoredUserListEventContent,
30 room::{
31 member::{MembershipState, RoomMemberEventContent},
32 power_levels::{PowerLevelAction, RoomPowerLevels, UserPowerLevel},
33 },
34 },
35};
36use tracing::debug;
37
38use super::Room;
39use crate::{
40 MinimalRoomMemberEvent, StateStore, StoreError,
41 deserialized_responses::{DisplayName, MemberEvent},
42 store::{Result as StoreResult, StateStoreExt, ambiguity_map::is_display_name_ambiguous},
43};
44
45impl Room {
46 pub fn are_members_synced(&self) -> bool {
53 self.info.read().members_synced
54 }
55
56 #[cfg(feature = "testing")]
61 pub fn mark_members_synced(&self) {
62 self.info.update(|info| {
63 info.members_synced = true;
64 });
65 }
66
67 pub fn mark_members_missing(&self) {
69 self.info.update_if(|info| {
70 mem::replace(&mut info.members_synced, false)
73 })
74 }
75
76 pub async fn members(&self, memberships: RoomMemberships) -> StoreResult<Vec<RoomMember>> {
79 let user_ids = self.store.get_user_ids(self.room_id(), memberships).await?;
80
81 if user_ids.is_empty() {
82 return Ok(Vec::new());
83 }
84
85 let member_events = async {
88 self.store
89 .get_state_events_for_keys_static::<RoomMemberEventContent, _, _>(
90 self.room_id(),
91 &user_ids,
92 )
93 .await?
94 .into_iter()
95 .map(|raw_event| raw_event.deserialize())
96 .collect::<Result<Vec<_>, _>>()
97 .map_err(StoreError::from)
98 };
99
100 let profiles = self.store.get_profiles(self.room_id(), &user_ids);
101
102 #[cfg(feature = "unstable-msc4426")]
103 let (member_events, mut profiles, mut global_profiles) =
104 future::try_join3(member_events, profiles, self.store.get_global_profiles(&user_ids))
105 .await?;
106
107 #[cfg(not(feature = "unstable-msc4426"))]
108 let (member_events, mut profiles) = future::try_join(member_events, profiles).await?;
109
110 let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
111 let room_info = self.member_room_info(&display_names).await?;
112
113 let mut members = Vec::with_capacity(member_events.len());
114
115 for (event, display_name) in member_events.into_iter().zip(&display_names) {
116 let profile = profiles.remove(event.user_id());
117 #[cfg(feature = "unstable-msc4426")]
118 let global_profile = global_profiles.remove(event.user_id());
119 members.push(RoomMember::from_parts(
120 event,
121 profile,
122 #[cfg(feature = "unstable-msc4426")]
123 global_profile,
124 display_name,
125 &room_info,
126 ))
127 }
128
129 Ok(members)
130 }
131
132 pub async fn human_member_ids(
135 &self,
136 memberships: RoomMemberships,
137 ) -> StoreResult<Vec<OwnedUserId>> {
138 let user_ids = self.store.get_user_ids(self.room_id(), memberships).await?;
139
140 let Some(service_members) = self.service_members() else {
141 return Ok(user_ids);
142 };
143
144 Ok(user_ids.into_iter().filter(|user_id| !service_members.contains(user_id)).collect())
145 }
146
147 pub fn active_members_count(&self) -> u64 {
150 self.info.read().active_members_count()
151 }
152
153 pub fn invited_members_count(&self) -> u64 {
155 self.info.read().invited_members_count()
156 }
157
158 pub fn joined_members_count(&self) -> u64 {
160 self.info.read().joined_members_count()
161 }
162
163 pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
171 let event = async {
172 let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await?
173 else {
174 debug!(%user_id, "Member event not found in state store");
175 return Ok(None);
176 };
177
178 Ok(Some(raw_event.deserialize()?))
179 };
180
181 let profile = self.store.get_profile(self.room_id(), user_id);
182
183 #[cfg(feature = "unstable-msc4426")]
184 let (Some(event), profile, global_profile) = future::try_join3(event, profile, async {
185 self.store.get_global_profile(user_id).await
186 })
187 .await?
188 else {
189 return Ok(None);
190 };
191 #[cfg(not(feature = "unstable-msc4426"))]
192 let (Some(event), profile) = future::try_join(event, profile).await? else {
193 return Ok(None);
194 };
195
196 let display_name = event.display_name();
197 let room_info = self.member_room_info(slice::from_ref(&display_name)).await?;
198
199 Ok(Some(RoomMember::from_parts(
200 event,
201 profile,
202 #[cfg(feature = "unstable-msc4426")]
203 global_profile,
204 &display_name,
205 &room_info,
206 )))
207 }
208
209 async fn member_room_info<'a>(
213 &self,
214 display_names: &'a [DisplayName],
215 ) -> StoreResult<MemberRoomInfo<'a>> {
216 let max_power_level = self.max_power_level();
217 let power_levels = async { Ok(self.power_levels_or_default().await) };
218
219 let users_display_names =
220 self.store.get_users_with_display_names(self.room_id(), display_names);
221
222 let ignored_users = async {
223 Ok(self
224 .store
225 .get_account_data_event_static::<IgnoredUserListEventContent>()
226 .await?
227 .map(|c| c.deserialize())
228 .transpose()?
229 .map(|e| e.content.ignored_users.into_keys().collect()))
230 };
231
232 let (power_levels, users_display_names, ignored_users) =
233 future::try_join3(power_levels, users_display_names, ignored_users).await?;
234
235 Ok(MemberRoomInfo {
236 power_levels: power_levels.into(),
237 max_power_level,
238 users_display_names,
239 ignored_users,
240 service_members: self.service_members(),
241 })
242 }
243}
244
245#[derive(Clone, Debug)]
247pub struct RoomMember {
248 pub(crate) event: Arc<MemberEvent>,
249 pub(crate) profile: Arc<Option<MinimalRoomMemberEvent>>,
253 #[cfg(feature = "unstable-msc4426")]
255 pub(crate) status: Option<StatusProfileField>,
256 #[cfg(feature = "unstable-msc4426")]
258 pub(crate) call: Option<CallProfileField>,
259 pub(crate) power_levels: Arc<RoomPowerLevels>,
260 pub(crate) max_power_level: i64,
261 pub(crate) display_name_ambiguous: bool,
262 pub(crate) is_ignored: bool,
263 pub(crate) is_service_member: bool,
264}
265
266impl RoomMember {
267 pub(crate) fn from_parts(
272 event: MemberEvent,
273 profile: Option<MinimalRoomMemberEvent>,
274 #[cfg(feature = "unstable-msc4426")] global_profile: Option<UserProfile>,
275 display_name: &DisplayName,
276 room_info: &MemberRoomInfo<'_>,
277 ) -> Self {
278 let MemberRoomInfo {
279 power_levels,
280 max_power_level,
281 users_display_names,
282 ignored_users,
283 service_members,
284 } = room_info;
285
286 let user_id = event.user_id().to_owned();
287 let display_name_ambiguous = users_display_names
288 .get(display_name)
289 .is_some_and(|s| is_display_name_ambiguous(display_name, s));
290 let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id()));
291 let is_service_member = service_members.as_ref().is_some_and(|s| s.contains(&user_id));
292
293 #[cfg(feature = "unstable-msc4426")]
295 let status = global_profile.as_ref().and_then(|p| p.get_static::<Status>().ok().flatten());
296 #[cfg(feature = "unstable-msc4426")]
297 let call = global_profile.as_ref().and_then(|p| p.get_static::<Call>().ok().flatten());
298
299 Self {
300 event: event.into(),
301 profile: profile.into(),
302 #[cfg(feature = "unstable-msc4426")]
303 status,
304 #[cfg(feature = "unstable-msc4426")]
305 call,
306 power_levels: power_levels.clone(),
307 max_power_level: *max_power_level,
308 display_name_ambiguous,
309 is_ignored,
310 is_service_member,
311 }
312 }
313
314 pub fn user_id(&self) -> &UserId {
316 self.event.user_id()
317 }
318
319 pub fn event(&self) -> &Arc<MemberEvent> {
321 &self.event
322 }
323
324 pub fn display_name(&self) -> Option<&str> {
326 if let Some(p) = self.profile.as_ref() {
327 p.content.displayname.as_deref()
328 } else {
329 self.event.displayname_value()
330 }
331 }
332
333 pub fn name(&self) -> &str {
338 if let Some(d) = self.display_name() { d } else { self.user_id().localpart() }
339 }
340
341 pub fn avatar_url(&self) -> Option<&MxcUri> {
343 if let Some(p) = self.profile.as_ref() {
344 p.content.avatar_url.as_deref()
345 } else {
346 self.event.avatar_url()
347 }
348 }
349
350 #[cfg(feature = "unstable-msc4426")]
358 pub fn status(&self) -> Option<&StatusProfileField> {
359 self.status.as_ref()
360 }
361
362 #[cfg(feature = "unstable-msc4426")]
370 pub fn call(&self) -> Option<&CallProfileField> {
371 self.call.as_ref()
372 }
373
374 pub fn normalized_power_level(&self) -> UserPowerLevel {
380 let UserPowerLevel::Int(power_level) = self.power_level() else {
381 return UserPowerLevel::Infinite;
382 };
383
384 let normalized_power_level = if self.max_power_level > 0 {
385 normalize_power_level(power_level, self.max_power_level)
386 } else {
387 power_level
388 };
389
390 UserPowerLevel::Int(normalized_power_level)
391 }
392
393 pub fn power_level(&self) -> UserPowerLevel {
395 self.power_levels.for_user(self.user_id())
396 }
397
398 pub fn can_ban(&self) -> bool {
402 self.can_do_impl(|pls| pls.user_can_ban(self.user_id()))
403 }
404
405 pub fn can_invite(&self) -> bool {
409 self.can_do_impl(|pls| pls.user_can_invite(self.user_id()))
410 }
411
412 pub fn can_kick(&self) -> bool {
416 self.can_do_impl(|pls| pls.user_can_kick(self.user_id()))
417 }
418
419 pub fn can_redact_own(&self) -> bool {
423 self.can_do_impl(|pls| pls.user_can_redact_own_event(self.user_id()))
424 }
425
426 pub fn can_redact_other(&self) -> bool {
431 self.can_do_impl(|pls| pls.user_can_redact_event_of_other(self.user_id()))
432 }
433
434 pub fn can_send_message(&self, msg_type: MessageLikeEventType) -> bool {
438 self.can_do_impl(|pls| pls.user_can_send_message(self.user_id(), msg_type))
439 }
440
441 pub fn can_send_state(&self, state_type: StateEventType) -> bool {
445 self.can_do_impl(|pls| pls.user_can_send_state(self.user_id(), state_type))
446 }
447
448 pub fn can_pin_or_unpin_event(&self) -> bool {
450 self.can_send_state(StateEventType::RoomPinnedEvents)
451 }
452
453 pub fn can_trigger_room_notification(&self) -> bool {
459 self.can_do_impl(|pls| pls.user_can_trigger_room_notification(self.user_id()))
460 }
461
462 pub fn can_do(&self, action: PowerLevelAction) -> bool {
464 self.can_do_impl(|pls| pls.user_can_do(self.user_id(), action))
465 }
466
467 fn can_do_impl(&self, f: impl FnOnce(&RoomPowerLevels) -> bool) -> bool {
468 f(&self.power_levels)
469 }
470
471 pub fn name_ambiguous(&self) -> bool {
476 self.display_name_ambiguous
477 }
478
479 pub fn membership(&self) -> &MembershipState {
481 self.event.membership()
482 }
483
484 pub fn is_ignored(&self) -> bool {
486 self.is_ignored
487 }
488
489 pub fn is_service_member(&self) -> bool {
491 self.is_service_member
492 }
493}
494
495pub(crate) struct MemberRoomInfo<'a> {
497 pub(crate) power_levels: Arc<RoomPowerLevels>,
498 pub(crate) max_power_level: i64,
499 pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
500 pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
501 pub(crate) service_members: Option<BTreeSet<OwnedUserId>>,
502}
503
504#[derive(Debug, Clone)]
506pub enum RoomMembersUpdate {
507 FullReload,
509 Partial(BTreeSet<OwnedUserId>),
511}
512
513bitflags! {
514 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
519 pub struct RoomMemberships: u16 {
520 const JOIN = 0b00000001;
522 const INVITE = 0b00000010;
524 const KNOCK = 0b00000100;
526 const LEAVE = 0b00001000;
528 const BAN = 0b00010000;
530
531 const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
533 }
534}
535
536impl RoomMemberships {
537 pub fn matches(&self, membership: &MembershipState) -> bool {
539 if self.is_empty() {
540 return true;
541 }
542
543 let membership = match membership {
544 MembershipState::Ban => Self::BAN,
545 MembershipState::Invite => Self::INVITE,
546 MembershipState::Join => Self::JOIN,
547 MembershipState::Knock => Self::KNOCK,
548 MembershipState::Leave => Self::LEAVE,
549 _ => return false,
550 };
551
552 self.contains(membership)
553 }
554
555 pub fn as_vec(&self) -> Vec<MembershipState> {
557 let mut memberships = Vec::new();
558
559 if self.contains(Self::JOIN) {
560 memberships.push(MembershipState::Join);
561 }
562 if self.contains(Self::INVITE) {
563 memberships.push(MembershipState::Invite);
564 }
565 if self.contains(Self::KNOCK) {
566 memberships.push(MembershipState::Knock);
567 }
568 if self.contains(Self::LEAVE) {
569 memberships.push(MembershipState::Leave);
570 }
571 if self.contains(Self::BAN) {
572 memberships.push(MembershipState::Ban);
573 }
574
575 memberships
576 }
577}
578
579pub fn normalize_power_level(power_level: Int, max_power_level: i64) -> Int {
581 let mut power_level = i64::from(power_level);
582 power_level = (power_level * 100) / max_power_level;
583
584 Int::try_from(power_level.clamp(0, 100))
585 .expect("We clamped the normalized power level so they must fit into the Int")
586}
587
588#[cfg(test)]
589mod tests {
590 #[cfg(feature = "unstable-msc4426")]
591 use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK;
592 #[cfg(feature = "unstable-msc4426")]
593 use matrix_sdk_test::event_factory::EventFactory;
594 use proptest::prelude::*;
595 #[cfg(feature = "unstable-msc4426")]
596 use ruma::{
597 SecondsSinceUnixEpoch, events::room::power_levels::RoomPowerLevelsSource,
598 profile::ProfileFieldValue, user_id,
599 };
600
601 use super::*;
602
603 prop_compose! {
604 fn arb_int()(id in any::<i64>()) -> Int {
605 id.try_into().unwrap_or_default()
606 }
607 }
608
609 proptest! {
610 #![proptest_config(ProptestConfig::with_cases(10_000))]
611 #[test]
612 fn test_power_level_normalization_with_min_max_level(power_level in arb_int()) {
613 let normalized = normalize_power_level(power_level, 1);
614 let normalized = i64::from(normalized);
615
616 assert!(normalized >= 0);
617 assert!(normalized <= 100);
618 }
619 }
620
621 proptest! {
622 #![proptest_config(ProptestConfig::with_cases(10_000))]
623 #[test]
624 fn test_power_level_normalization(power_level in arb_int(), max_level in 1i64..) {
625 let normalized = normalize_power_level(power_level, max_level);
626 let normalized = i64::from(normalized);
627
628 assert!(normalized >= 0);
629 assert!(normalized <= 100);
630 }
631 }
632
633 #[test]
634 fn test_power_level_normalization_limits() {
635 let level = Int::MIN;
636 let normalized = normalize_power_level(level, 1);
637 let normalized = i64::from(normalized);
638 assert!(normalized >= 0);
639 assert!(normalized <= 100);
640
641 let level = Int::MAX;
642 let normalized = normalize_power_level(level, 1);
643 let normalized = i64::from(normalized);
644 assert!(normalized >= 0);
645 assert!(normalized <= 100);
646 }
647
648 #[cfg(feature = "unstable-msc4426")]
649 #[test]
650 fn test_global_profile_fields() {
651 let user_id = user_id!("@alice:example.org");
652 let event = MemberEvent::Sync(EventFactory::new().sender(user_id).member(user_id).into());
653 let room_info = MemberRoomInfo {
654 power_levels: Arc::new(RoomPowerLevels::new(
655 RoomPowerLevelsSource::None,
656 &ROOM_VERSION_RULES_FALLBACK.authorization,
657 std::iter::empty::<OwnedUserId>(),
658 )),
659 max_power_level: 100,
660 users_display_names: HashMap::new(),
661 ignored_users: None,
662 service_members: None,
663 };
664
665 let member =
667 RoomMember::from_parts(event.clone(), None, None, &event.display_name(), &room_info);
668 assert!(member.status().is_none());
669 assert!(member.call().is_none());
670
671 let mut call = CallProfileField::new();
674 call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()));
675 let global_profile = UserProfile::from_iter([
676 ProfileFieldValue::Status(StatusProfileField::new(
677 "Working".to_owned(),
678 "💻".to_owned(),
679 )),
680 ProfileFieldValue::Call(call),
681 ]);
682
683 let display_name = event.display_name();
684 let member =
685 RoomMember::from_parts(event, None, Some(global_profile), &display_name, &room_info);
686
687 let status = member.status().expect("status is set");
688 assert_eq!(status.text, "Working");
689 assert_eq!(status.emoji, "💻");
690 assert_eq!(
691 member.call().expect("call is set").call_joined_ts,
692 Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()))
693 );
694 }
695}