1use std::{
16 collections::{BTreeMap, BTreeSet, HashMap},
17 mem,
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 presence::PresenceEvent,
31 room::{
32 member::{MembershipState, RoomMemberEventContent},
33 power_levels::{PowerLevelAction, RoomPowerLevels, UserPowerLevel},
34 },
35 },
36};
37use tracing::debug;
38
39use super::Room;
40use crate::{
41 MinimalRoomMemberEvent, StateStore, StoreError,
42 deserialized_responses::{DisplayName, MemberEvent},
43 store::{Result as StoreResult, StateStoreExt, ambiguity_map::is_display_name_ambiguous},
44};
45
46impl Room {
47 pub fn are_members_synced(&self) -> bool {
54 self.info.read().members_synced
55 }
56
57 #[cfg(feature = "testing")]
62 pub fn mark_members_synced(&self) {
63 self.info.update(|info| {
64 info.members_synced = true;
65 });
66 }
67
68 pub fn mark_members_missing(&self) {
70 self.info.update_if(|info| {
71 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 = self
86 .store
87 .get_state_events_for_keys_static::<RoomMemberEventContent, _, _>(
88 self.room_id(),
89 &user_ids,
90 )
91 .await?
92 .into_iter()
93 .map(|raw_event| raw_event.deserialize())
94 .collect::<Result<Vec<_>, _>>()?;
95
96 let mut profiles = self.store.get_profiles(self.room_id(), &user_ids).await?;
97
98 #[cfg(feature = "unstable-msc4426")]
99 let mut global_profiles = self.store.get_global_profiles(&user_ids).await?;
100
101 let mut presences = self
102 .store
103 .get_presence_events(&user_ids)
104 .await?
105 .into_iter()
106 .filter_map(|e| {
107 e.deserialize().ok().map(|presence| (presence.sender.clone(), presence))
108 })
109 .collect::<BTreeMap<_, _>>();
110
111 let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
112 let room_info = self.member_room_info(&display_names).await?;
113
114 let mut members = Vec::new();
115
116 for event in member_events {
117 let profile = profiles.remove(event.user_id());
118 #[cfg(feature = "unstable-msc4426")]
119 let global_profile = global_profiles.remove(event.user_id());
120 let presence = presences.remove(event.user_id());
121 members.push(RoomMember::from_parts(
122 event,
123 profile,
124 #[cfg(feature = "unstable-msc4426")]
125 global_profile,
126 presence,
127 &room_info,
128 ))
129 }
130
131 Ok(members)
132 }
133
134 pub fn active_members_count(&self) -> u64 {
137 self.info.read().active_members_count()
138 }
139
140 pub fn invited_members_count(&self) -> u64 {
142 self.info.read().invited_members_count()
143 }
144
145 pub fn joined_members_count(&self) -> u64 {
147 self.info.read().joined_members_count()
148 }
149
150 pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
158 let event = async {
159 let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await?
160 else {
161 debug!(%user_id, "Member event not found in state store");
162 return Ok(None);
163 };
164
165 Ok(Some(raw_event.deserialize()?))
166 };
167 let presence = async {
168 let raw_event = self.store.get_presence_event(user_id).await?;
169 Ok::<Option<PresenceEvent>, StoreError>(raw_event.and_then(|e| e.deserialize().ok()))
170 };
171
172 let profile = async { self.store.get_profile(self.room_id(), user_id).await };
173
174 #[cfg(feature = "unstable-msc4426")]
175 let (Some(event), presence, profile, global_profile) =
176 future::try_join4(event, presence, profile, async {
177 self.store.get_global_profile(user_id).await
178 })
179 .await?
180 else {
181 return Ok(None);
182 };
183 #[cfg(not(feature = "unstable-msc4426"))]
184 let (Some(event), presence, profile) = future::try_join3(event, presence, profile).await?
185 else {
186 return Ok(None);
187 };
188
189 let display_names = [event.display_name()];
190 let room_info = self.member_room_info(&display_names).await?;
191
192 Ok(Some(RoomMember::from_parts(
193 event,
194 profile,
195 #[cfg(feature = "unstable-msc4426")]
196 global_profile,
197 presence,
198 &room_info,
199 )))
200 }
201
202 async fn member_room_info<'a>(
206 &self,
207 display_names: &'a [DisplayName],
208 ) -> StoreResult<MemberRoomInfo<'a>> {
209 let max_power_level = self.max_power_level();
210 let power_levels = async { Ok(self.power_levels_or_default().await) };
211
212 let users_display_names =
213 self.store.get_users_with_display_names(self.room_id(), display_names);
214
215 let ignored_users = async {
216 Ok(self
217 .store
218 .get_account_data_event_static::<IgnoredUserListEventContent>()
219 .await?
220 .map(|c| c.deserialize())
221 .transpose()?
222 .map(|e| e.content.ignored_users.into_keys().collect()))
223 };
224
225 let (power_levels, users_display_names, ignored_users) =
226 future::try_join3(power_levels, users_display_names, ignored_users).await?;
227
228 Ok(MemberRoomInfo {
229 power_levels: power_levels.into(),
230 max_power_level,
231 users_display_names,
232 ignored_users,
233 service_members: self.service_members(),
234 })
235 }
236}
237
238#[derive(Clone, Debug)]
240pub struct RoomMember {
241 pub(crate) event: Arc<MemberEvent>,
242 pub(crate) profile: Arc<Option<MinimalRoomMemberEvent>>,
246 #[cfg(feature = "unstable-msc4426")]
248 pub(crate) status: Option<StatusProfileField>,
249 #[cfg(feature = "unstable-msc4426")]
251 pub(crate) call: Option<CallProfileField>,
252 #[allow(dead_code)]
253 pub(crate) presence: Arc<Option<PresenceEvent>>,
254 pub(crate) power_levels: Arc<RoomPowerLevels>,
255 pub(crate) max_power_level: i64,
256 pub(crate) display_name_ambiguous: bool,
257 pub(crate) is_ignored: bool,
258 pub(crate) is_service_member: bool,
259}
260
261impl RoomMember {
262 pub(crate) fn from_parts(
263 event: MemberEvent,
264 profile: Option<MinimalRoomMemberEvent>,
265 #[cfg(feature = "unstable-msc4426")] global_profile: Option<UserProfile>,
266 presence: Option<PresenceEvent>,
267 room_info: &MemberRoomInfo<'_>,
268 ) -> Self {
269 let MemberRoomInfo {
270 power_levels,
271 max_power_level,
272 users_display_names,
273 ignored_users,
274 service_members,
275 } = room_info;
276
277 let user_id = event.user_id().to_owned();
278 let display_name = event.display_name();
279 let display_name_ambiguous = users_display_names
280 .get(&display_name)
281 .is_some_and(|s| is_display_name_ambiguous(&display_name, s));
282 let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id()));
283 let is_service_member = service_members.as_ref().is_some_and(|s| s.contains(&user_id));
284
285 #[cfg(feature = "unstable-msc4426")]
287 let status = global_profile.as_ref().and_then(|p| p.get_static::<Status>().ok().flatten());
288 #[cfg(feature = "unstable-msc4426")]
289 let call = global_profile.as_ref().and_then(|p| p.get_static::<Call>().ok().flatten());
290
291 Self {
292 event: event.into(),
293 profile: profile.into(),
294 #[cfg(feature = "unstable-msc4426")]
295 status,
296 #[cfg(feature = "unstable-msc4426")]
297 call,
298 presence: presence.into(),
299 power_levels: power_levels.clone(),
300 max_power_level: *max_power_level,
301 display_name_ambiguous,
302 is_ignored,
303 is_service_member,
304 }
305 }
306
307 pub fn user_id(&self) -> &UserId {
309 self.event.user_id()
310 }
311
312 pub fn event(&self) -> &Arc<MemberEvent> {
314 &self.event
315 }
316
317 pub fn display_name(&self) -> Option<&str> {
319 if let Some(p) = self.profile.as_ref() {
320 p.content.displayname.as_deref()
321 } else {
322 self.event.displayname_value()
323 }
324 }
325
326 pub fn name(&self) -> &str {
331 if let Some(d) = self.display_name() { d } else { self.user_id().localpart() }
332 }
333
334 pub fn avatar_url(&self) -> Option<&MxcUri> {
336 if let Some(p) = self.profile.as_ref() {
337 p.content.avatar_url.as_deref()
338 } else {
339 self.event.avatar_url()
340 }
341 }
342
343 #[cfg(feature = "unstable-msc4426")]
351 pub fn status(&self) -> Option<&StatusProfileField> {
352 self.status.as_ref()
353 }
354
355 #[cfg(feature = "unstable-msc4426")]
363 pub fn call(&self) -> Option<&CallProfileField> {
364 self.call.as_ref()
365 }
366
367 pub fn normalized_power_level(&self) -> UserPowerLevel {
373 let UserPowerLevel::Int(power_level) = self.power_level() else {
374 return UserPowerLevel::Infinite;
375 };
376
377 let normalized_power_level = if self.max_power_level > 0 {
378 normalize_power_level(power_level, self.max_power_level)
379 } else {
380 power_level
381 };
382
383 UserPowerLevel::Int(normalized_power_level)
384 }
385
386 pub fn power_level(&self) -> UserPowerLevel {
388 self.power_levels.for_user(self.user_id())
389 }
390
391 pub fn can_ban(&self) -> bool {
395 self.can_do_impl(|pls| pls.user_can_ban(self.user_id()))
396 }
397
398 pub fn can_invite(&self) -> bool {
402 self.can_do_impl(|pls| pls.user_can_invite(self.user_id()))
403 }
404
405 pub fn can_kick(&self) -> bool {
409 self.can_do_impl(|pls| pls.user_can_kick(self.user_id()))
410 }
411
412 pub fn can_redact_own(&self) -> bool {
416 self.can_do_impl(|pls| pls.user_can_redact_own_event(self.user_id()))
417 }
418
419 pub fn can_redact_other(&self) -> bool {
424 self.can_do_impl(|pls| pls.user_can_redact_event_of_other(self.user_id()))
425 }
426
427 pub fn can_send_message(&self, msg_type: MessageLikeEventType) -> bool {
431 self.can_do_impl(|pls| pls.user_can_send_message(self.user_id(), msg_type))
432 }
433
434 pub fn can_send_state(&self, state_type: StateEventType) -> bool {
438 self.can_do_impl(|pls| pls.user_can_send_state(self.user_id(), state_type))
439 }
440
441 pub fn can_pin_or_unpin_event(&self) -> bool {
443 self.can_send_state(StateEventType::RoomPinnedEvents)
444 }
445
446 pub fn can_trigger_room_notification(&self) -> bool {
452 self.can_do_impl(|pls| pls.user_can_trigger_room_notification(self.user_id()))
453 }
454
455 pub fn can_do(&self, action: PowerLevelAction) -> bool {
458 self.can_do_impl(|pls| pls.user_can_do(self.user_id(), action))
459 }
460
461 fn can_do_impl(&self, f: impl FnOnce(&RoomPowerLevels) -> bool) -> bool {
462 f(&self.power_levels)
463 }
464
465 pub fn name_ambiguous(&self) -> bool {
470 self.display_name_ambiguous
471 }
472
473 pub fn membership(&self) -> &MembershipState {
475 self.event.membership()
476 }
477
478 pub fn is_ignored(&self) -> bool {
480 self.is_ignored
481 }
482
483 pub fn is_service_member(&self) -> bool {
485 self.is_service_member
486 }
487}
488
489pub(crate) struct MemberRoomInfo<'a> {
491 pub(crate) power_levels: Arc<RoomPowerLevels>,
492 pub(crate) max_power_level: i64,
493 pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
494 pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
495 pub(crate) service_members: Option<BTreeSet<OwnedUserId>>,
496}
497
498#[derive(Debug, Clone)]
500pub enum RoomMembersUpdate {
501 FullReload,
503 Partial(BTreeSet<OwnedUserId>),
505}
506
507bitflags! {
508 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
513 pub struct RoomMemberships: u16 {
514 const JOIN = 0b00000001;
516 const INVITE = 0b00000010;
518 const KNOCK = 0b00000100;
520 const LEAVE = 0b00001000;
522 const BAN = 0b00010000;
524
525 const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
527 }
528}
529
530impl RoomMemberships {
531 pub fn matches(&self, membership: &MembershipState) -> bool {
533 if self.is_empty() {
534 return true;
535 }
536
537 let membership = match membership {
538 MembershipState::Ban => Self::BAN,
539 MembershipState::Invite => Self::INVITE,
540 MembershipState::Join => Self::JOIN,
541 MembershipState::Knock => Self::KNOCK,
542 MembershipState::Leave => Self::LEAVE,
543 _ => return false,
544 };
545
546 self.contains(membership)
547 }
548
549 pub fn as_vec(&self) -> Vec<MembershipState> {
551 let mut memberships = Vec::new();
552
553 if self.contains(Self::JOIN) {
554 memberships.push(MembershipState::Join);
555 }
556 if self.contains(Self::INVITE) {
557 memberships.push(MembershipState::Invite);
558 }
559 if self.contains(Self::KNOCK) {
560 memberships.push(MembershipState::Knock);
561 }
562 if self.contains(Self::LEAVE) {
563 memberships.push(MembershipState::Leave);
564 }
565 if self.contains(Self::BAN) {
566 memberships.push(MembershipState::Ban);
567 }
568
569 memberships
570 }
571}
572
573pub fn normalize_power_level(power_level: Int, max_power_level: i64) -> Int {
575 let mut power_level = i64::from(power_level);
576 power_level = (power_level * 100) / max_power_level;
577
578 Int::try_from(power_level.clamp(0, 100))
579 .expect("We clamped the normalized power level so they must fit into the Int")
580}
581
582#[cfg(test)]
583mod tests {
584 #[cfg(feature = "unstable-msc4426")]
585 use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK;
586 #[cfg(feature = "unstable-msc4426")]
587 use matrix_sdk_test::event_factory::EventFactory;
588 use proptest::prelude::*;
589 #[cfg(feature = "unstable-msc4426")]
590 use ruma::{
591 SecondsSinceUnixEpoch, events::room::power_levels::RoomPowerLevelsSource,
592 profile::ProfileFieldValue, user_id,
593 };
594
595 use super::*;
596
597 prop_compose! {
598 fn arb_int()(id in any::<i64>()) -> Int {
599 id.try_into().unwrap_or_default()
600 }
601 }
602
603 proptest! {
604 #![proptest_config(ProptestConfig::with_cases(10_000))]
605 #[test]
606 fn test_power_level_normalization_with_min_max_level(power_level in arb_int()) {
607 let normalized = normalize_power_level(power_level, 1);
608 let normalized = i64::from(normalized);
609
610 assert!(normalized >= 0);
611 assert!(normalized <= 100);
612 }
613 }
614
615 proptest! {
616 #![proptest_config(ProptestConfig::with_cases(10_000))]
617 #[test]
618 fn test_power_level_normalization(power_level in arb_int(), max_level in 1i64..) {
619 let normalized = normalize_power_level(power_level, max_level);
620 let normalized = i64::from(normalized);
621
622 assert!(normalized >= 0);
623 assert!(normalized <= 100);
624 }
625 }
626
627 #[test]
628 fn test_power_level_normalization_limits() {
629 let level = Int::MIN;
630 let normalized = normalize_power_level(level, 1);
631 let normalized = i64::from(normalized);
632 assert!(normalized >= 0);
633 assert!(normalized <= 100);
634
635 let level = Int::MAX;
636 let normalized = normalize_power_level(level, 1);
637 let normalized = i64::from(normalized);
638 assert!(normalized >= 0);
639 assert!(normalized <= 100);
640 }
641
642 #[cfg(feature = "unstable-msc4426")]
643 #[test]
644 fn test_global_profile_fields() {
645 let user_id = user_id!("@alice:example.org");
646 let event = MemberEvent::Sync(EventFactory::new().sender(user_id).member(user_id).into());
647 let room_info = MemberRoomInfo {
648 power_levels: Arc::new(RoomPowerLevels::new(
649 RoomPowerLevelsSource::None,
650 &ROOM_VERSION_RULES_FALLBACK.authorization,
651 std::iter::empty::<OwnedUserId>(),
652 )),
653 max_power_level: 100,
654 users_display_names: HashMap::new(),
655 ignored_users: None,
656 service_members: None,
657 };
658
659 let member = RoomMember::from_parts(event.clone(), None, None, None, &room_info);
661 assert!(member.status().is_none());
662 assert!(member.call().is_none());
663
664 let mut call = CallProfileField::new();
667 call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()));
668 let global_profile = UserProfile::from_iter([
669 ProfileFieldValue::Status(StatusProfileField::new(
670 "Working".to_owned(),
671 "💻".to_owned(),
672 )),
673 ProfileFieldValue::Call(call),
674 ]);
675
676 let member = RoomMember::from_parts(event, None, Some(global_profile), None, &room_info);
677
678 let status = member.status().expect("status is set");
679 assert_eq!(status.text, "Working");
680 assert_eq!(status.emoji, "💻");
681 assert_eq!(
682 member.call().expect("call is set").call_joined_ts,
683 Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()))
684 );
685 }
686}