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 async fn human_member_ids(
137 &self,
138 memberships: RoomMemberships,
139 ) -> StoreResult<Vec<OwnedUserId>> {
140 let user_ids = self.store.get_user_ids(self.room_id(), memberships).await?;
141
142 let Some(service_members) = self.service_members() else {
143 return Ok(user_ids);
144 };
145
146 Ok(user_ids.into_iter().filter(|user_id| !service_members.contains(user_id)).collect())
147 }
148
149 pub fn active_members_count(&self) -> u64 {
152 self.info.read().active_members_count()
153 }
154
155 pub fn invited_members_count(&self) -> u64 {
157 self.info.read().invited_members_count()
158 }
159
160 pub fn joined_members_count(&self) -> u64 {
162 self.info.read().joined_members_count()
163 }
164
165 pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
173 let event = async {
174 let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await?
175 else {
176 debug!(%user_id, "Member event not found in state store");
177 return Ok(None);
178 };
179
180 Ok(Some(raw_event.deserialize()?))
181 };
182 let presence = async {
183 let raw_event = self.store.get_presence_event(user_id).await?;
184 Ok::<Option<PresenceEvent>, StoreError>(raw_event.and_then(|e| e.deserialize().ok()))
185 };
186
187 let profile = async { self.store.get_profile(self.room_id(), user_id).await };
188
189 #[cfg(feature = "unstable-msc4426")]
190 let (Some(event), presence, profile, global_profile) =
191 future::try_join4(event, presence, profile, async {
192 self.store.get_global_profile(user_id).await
193 })
194 .await?
195 else {
196 return Ok(None);
197 };
198 #[cfg(not(feature = "unstable-msc4426"))]
199 let (Some(event), presence, profile) = future::try_join3(event, presence, profile).await?
200 else {
201 return Ok(None);
202 };
203
204 let display_names = [event.display_name()];
205 let room_info = self.member_room_info(&display_names).await?;
206
207 Ok(Some(RoomMember::from_parts(
208 event,
209 profile,
210 #[cfg(feature = "unstable-msc4426")]
211 global_profile,
212 presence,
213 &room_info,
214 )))
215 }
216
217 async fn member_room_info<'a>(
221 &self,
222 display_names: &'a [DisplayName],
223 ) -> StoreResult<MemberRoomInfo<'a>> {
224 let max_power_level = self.max_power_level();
225 let power_levels = async { Ok(self.power_levels_or_default().await) };
226
227 let users_display_names =
228 self.store.get_users_with_display_names(self.room_id(), display_names);
229
230 let ignored_users = async {
231 Ok(self
232 .store
233 .get_account_data_event_static::<IgnoredUserListEventContent>()
234 .await?
235 .map(|c| c.deserialize())
236 .transpose()?
237 .map(|e| e.content.ignored_users.into_keys().collect()))
238 };
239
240 let (power_levels, users_display_names, ignored_users) =
241 future::try_join3(power_levels, users_display_names, ignored_users).await?;
242
243 Ok(MemberRoomInfo {
244 power_levels: power_levels.into(),
245 max_power_level,
246 users_display_names,
247 ignored_users,
248 service_members: self.service_members(),
249 })
250 }
251}
252
253#[derive(Clone, Debug)]
255pub struct RoomMember {
256 pub(crate) event: Arc<MemberEvent>,
257 pub(crate) profile: Arc<Option<MinimalRoomMemberEvent>>,
261 #[cfg(feature = "unstable-msc4426")]
263 pub(crate) status: Option<StatusProfileField>,
264 #[cfg(feature = "unstable-msc4426")]
266 pub(crate) call: Option<CallProfileField>,
267 #[allow(dead_code)]
268 pub(crate) presence: Arc<Option<PresenceEvent>>,
269 pub(crate) power_levels: Arc<RoomPowerLevels>,
270 pub(crate) max_power_level: i64,
271 pub(crate) display_name_ambiguous: bool,
272 pub(crate) is_ignored: bool,
273 pub(crate) is_service_member: bool,
274}
275
276impl RoomMember {
277 pub(crate) fn from_parts(
278 event: MemberEvent,
279 profile: Option<MinimalRoomMemberEvent>,
280 #[cfg(feature = "unstable-msc4426")] global_profile: Option<UserProfile>,
281 presence: Option<PresenceEvent>,
282 room_info: &MemberRoomInfo<'_>,
283 ) -> Self {
284 let MemberRoomInfo {
285 power_levels,
286 max_power_level,
287 users_display_names,
288 ignored_users,
289 service_members,
290 } = room_info;
291
292 let user_id = event.user_id().to_owned();
293 let display_name = event.display_name();
294 let display_name_ambiguous = users_display_names
295 .get(&display_name)
296 .is_some_and(|s| is_display_name_ambiguous(&display_name, s));
297 let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id()));
298 let is_service_member = service_members.as_ref().is_some_and(|s| s.contains(&user_id));
299
300 #[cfg(feature = "unstable-msc4426")]
302 let status = global_profile.as_ref().and_then(|p| p.get_static::<Status>().ok().flatten());
303 #[cfg(feature = "unstable-msc4426")]
304 let call = global_profile.as_ref().and_then(|p| p.get_static::<Call>().ok().flatten());
305
306 Self {
307 event: event.into(),
308 profile: profile.into(),
309 #[cfg(feature = "unstable-msc4426")]
310 status,
311 #[cfg(feature = "unstable-msc4426")]
312 call,
313 presence: presence.into(),
314 power_levels: power_levels.clone(),
315 max_power_level: *max_power_level,
316 display_name_ambiguous,
317 is_ignored,
318 is_service_member,
319 }
320 }
321
322 pub fn user_id(&self) -> &UserId {
324 self.event.user_id()
325 }
326
327 pub fn event(&self) -> &Arc<MemberEvent> {
329 &self.event
330 }
331
332 pub fn display_name(&self) -> Option<&str> {
334 if let Some(p) = self.profile.as_ref() {
335 p.content.displayname.as_deref()
336 } else {
337 self.event.displayname_value()
338 }
339 }
340
341 pub fn name(&self) -> &str {
346 if let Some(d) = self.display_name() { d } else { self.user_id().localpart() }
347 }
348
349 pub fn avatar_url(&self) -> Option<&MxcUri> {
351 if let Some(p) = self.profile.as_ref() {
352 p.content.avatar_url.as_deref()
353 } else {
354 self.event.avatar_url()
355 }
356 }
357
358 #[cfg(feature = "unstable-msc4426")]
366 pub fn status(&self) -> Option<&StatusProfileField> {
367 self.status.as_ref()
368 }
369
370 #[cfg(feature = "unstable-msc4426")]
378 pub fn call(&self) -> Option<&CallProfileField> {
379 self.call.as_ref()
380 }
381
382 pub fn normalized_power_level(&self) -> UserPowerLevel {
388 let UserPowerLevel::Int(power_level) = self.power_level() else {
389 return UserPowerLevel::Infinite;
390 };
391
392 let normalized_power_level = if self.max_power_level > 0 {
393 normalize_power_level(power_level, self.max_power_level)
394 } else {
395 power_level
396 };
397
398 UserPowerLevel::Int(normalized_power_level)
399 }
400
401 pub fn power_level(&self) -> UserPowerLevel {
403 self.power_levels.for_user(self.user_id())
404 }
405
406 pub fn can_ban(&self) -> bool {
410 self.can_do_impl(|pls| pls.user_can_ban(self.user_id()))
411 }
412
413 pub fn can_invite(&self) -> bool {
417 self.can_do_impl(|pls| pls.user_can_invite(self.user_id()))
418 }
419
420 pub fn can_kick(&self) -> bool {
424 self.can_do_impl(|pls| pls.user_can_kick(self.user_id()))
425 }
426
427 pub fn can_redact_own(&self) -> bool {
431 self.can_do_impl(|pls| pls.user_can_redact_own_event(self.user_id()))
432 }
433
434 pub fn can_redact_other(&self) -> bool {
439 self.can_do_impl(|pls| pls.user_can_redact_event_of_other(self.user_id()))
440 }
441
442 pub fn can_send_message(&self, msg_type: MessageLikeEventType) -> bool {
446 self.can_do_impl(|pls| pls.user_can_send_message(self.user_id(), msg_type))
447 }
448
449 pub fn can_send_state(&self, state_type: StateEventType) -> bool {
453 self.can_do_impl(|pls| pls.user_can_send_state(self.user_id(), state_type))
454 }
455
456 pub fn can_pin_or_unpin_event(&self) -> bool {
458 self.can_send_state(StateEventType::RoomPinnedEvents)
459 }
460
461 pub fn can_trigger_room_notification(&self) -> bool {
467 self.can_do_impl(|pls| pls.user_can_trigger_room_notification(self.user_id()))
468 }
469
470 pub fn can_do(&self, action: PowerLevelAction) -> bool {
473 self.can_do_impl(|pls| pls.user_can_do(self.user_id(), action))
474 }
475
476 fn can_do_impl(&self, f: impl FnOnce(&RoomPowerLevels) -> bool) -> bool {
477 f(&self.power_levels)
478 }
479
480 pub fn name_ambiguous(&self) -> bool {
485 self.display_name_ambiguous
486 }
487
488 pub fn membership(&self) -> &MembershipState {
490 self.event.membership()
491 }
492
493 pub fn is_ignored(&self) -> bool {
495 self.is_ignored
496 }
497
498 pub fn is_service_member(&self) -> bool {
500 self.is_service_member
501 }
502}
503
504pub(crate) struct MemberRoomInfo<'a> {
506 pub(crate) power_levels: Arc<RoomPowerLevels>,
507 pub(crate) max_power_level: i64,
508 pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
509 pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
510 pub(crate) service_members: Option<BTreeSet<OwnedUserId>>,
511}
512
513#[derive(Debug, Clone)]
515pub enum RoomMembersUpdate {
516 FullReload,
518 Partial(BTreeSet<OwnedUserId>),
520}
521
522bitflags! {
523 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
528 pub struct RoomMemberships: u16 {
529 const JOIN = 0b00000001;
531 const INVITE = 0b00000010;
533 const KNOCK = 0b00000100;
535 const LEAVE = 0b00001000;
537 const BAN = 0b00010000;
539
540 const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
542 }
543}
544
545impl RoomMemberships {
546 pub fn matches(&self, membership: &MembershipState) -> bool {
548 if self.is_empty() {
549 return true;
550 }
551
552 let membership = match membership {
553 MembershipState::Ban => Self::BAN,
554 MembershipState::Invite => Self::INVITE,
555 MembershipState::Join => Self::JOIN,
556 MembershipState::Knock => Self::KNOCK,
557 MembershipState::Leave => Self::LEAVE,
558 _ => return false,
559 };
560
561 self.contains(membership)
562 }
563
564 pub fn as_vec(&self) -> Vec<MembershipState> {
566 let mut memberships = Vec::new();
567
568 if self.contains(Self::JOIN) {
569 memberships.push(MembershipState::Join);
570 }
571 if self.contains(Self::INVITE) {
572 memberships.push(MembershipState::Invite);
573 }
574 if self.contains(Self::KNOCK) {
575 memberships.push(MembershipState::Knock);
576 }
577 if self.contains(Self::LEAVE) {
578 memberships.push(MembershipState::Leave);
579 }
580 if self.contains(Self::BAN) {
581 memberships.push(MembershipState::Ban);
582 }
583
584 memberships
585 }
586}
587
588pub fn normalize_power_level(power_level: Int, max_power_level: i64) -> Int {
590 let mut power_level = i64::from(power_level);
591 power_level = (power_level * 100) / max_power_level;
592
593 Int::try_from(power_level.clamp(0, 100))
594 .expect("We clamped the normalized power level so they must fit into the Int")
595}
596
597#[cfg(test)]
598mod tests {
599 #[cfg(feature = "unstable-msc4426")]
600 use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK;
601 #[cfg(feature = "unstable-msc4426")]
602 use matrix_sdk_test::event_factory::EventFactory;
603 use proptest::prelude::*;
604 #[cfg(feature = "unstable-msc4426")]
605 use ruma::{
606 SecondsSinceUnixEpoch, events::room::power_levels::RoomPowerLevelsSource,
607 profile::ProfileFieldValue, user_id,
608 };
609
610 use super::*;
611
612 prop_compose! {
613 fn arb_int()(id in any::<i64>()) -> Int {
614 id.try_into().unwrap_or_default()
615 }
616 }
617
618 proptest! {
619 #![proptest_config(ProptestConfig::with_cases(10_000))]
620 #[test]
621 fn test_power_level_normalization_with_min_max_level(power_level in arb_int()) {
622 let normalized = normalize_power_level(power_level, 1);
623 let normalized = i64::from(normalized);
624
625 assert!(normalized >= 0);
626 assert!(normalized <= 100);
627 }
628 }
629
630 proptest! {
631 #![proptest_config(ProptestConfig::with_cases(10_000))]
632 #[test]
633 fn test_power_level_normalization(power_level in arb_int(), max_level in 1i64..) {
634 let normalized = normalize_power_level(power_level, max_level);
635 let normalized = i64::from(normalized);
636
637 assert!(normalized >= 0);
638 assert!(normalized <= 100);
639 }
640 }
641
642 #[test]
643 fn test_power_level_normalization_limits() {
644 let level = Int::MIN;
645 let normalized = normalize_power_level(level, 1);
646 let normalized = i64::from(normalized);
647 assert!(normalized >= 0);
648 assert!(normalized <= 100);
649
650 let level = Int::MAX;
651 let normalized = normalize_power_level(level, 1);
652 let normalized = i64::from(normalized);
653 assert!(normalized >= 0);
654 assert!(normalized <= 100);
655 }
656
657 #[cfg(feature = "unstable-msc4426")]
658 #[test]
659 fn test_global_profile_fields() {
660 let user_id = user_id!("@alice:example.org");
661 let event = MemberEvent::Sync(EventFactory::new().sender(user_id).member(user_id).into());
662 let room_info = MemberRoomInfo {
663 power_levels: Arc::new(RoomPowerLevels::new(
664 RoomPowerLevelsSource::None,
665 &ROOM_VERSION_RULES_FALLBACK.authorization,
666 std::iter::empty::<OwnedUserId>(),
667 )),
668 max_power_level: 100,
669 users_display_names: HashMap::new(),
670 ignored_users: None,
671 service_members: None,
672 };
673
674 let member = RoomMember::from_parts(event.clone(), None, None, None, &room_info);
676 assert!(member.status().is_none());
677 assert!(member.call().is_none());
678
679 let mut call = CallProfileField::new();
682 call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()));
683 let global_profile = UserProfile::from_iter([
684 ProfileFieldValue::Status(StatusProfileField::new(
685 "Working".to_owned(),
686 "💻".to_owned(),
687 )),
688 ProfileFieldValue::Call(call),
689 ]);
690
691 let member = RoomMember::from_parts(event, None, Some(global_profile), None, &room_info);
692
693 let status = member.status().expect("status is set");
694 assert_eq!(status.text, "Working");
695 assert_eq!(status.emoji, "💻");
696 assert_eq!(
697 member.call().expect("call is set").call_joined_ts,
698 Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()))
699 );
700 }
701}