Skip to main content

matrix_sdk_base/room/
members.rs

1// Copyright 2020 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /// Check if the room has its members fully synced.
47    ///
48    /// Members might be missing if lazy member loading was enabled for the
49    /// sync.
50    ///
51    /// Returns true if no members are missing, false otherwise.
52    pub fn are_members_synced(&self) -> bool {
53        self.info.read().members_synced
54    }
55
56    /// Mark this Room as holding all member information.
57    ///
58    /// Useful in tests if we want to persuade the Room not to sync when asked
59    /// about its members.
60    #[cfg(feature = "testing")]
61    pub fn mark_members_synced(&self) {
62        self.info.update(|info| {
63            info.members_synced = true;
64        });
65    }
66
67    /// Mark this Room as still missing member information.
68    pub fn mark_members_missing(&self) {
69        self.info.update_if(|info| {
70            // notify observable subscribers only if the previous value was
71            // false
72            mem::replace(&mut info.members_synced, false)
73        })
74    }
75
76    /// Get the `RoomMember`s of this room that are known to the store, with the
77    /// given memberships.
78    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        // These store reads are independent of each other, so run them
86        // together rather than one after the other.
87        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    /// Get the user IDs of the members with the given memberships, without the
133    /// service members, see [`Self::service_members`].
134    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    /// Returns the number of members who have joined or been invited to the
148    /// room.
149    pub fn active_members_count(&self) -> u64 {
150        self.info.read().active_members_count()
151    }
152
153    /// Returns the number of members who have been invited to the room.
154    pub fn invited_members_count(&self) -> u64 {
155        self.info.read().invited_members_count()
156    }
157
158    /// Returns the number of members who have joined the room.
159    pub fn joined_members_count(&self) -> u64 {
160        self.info.read().joined_members_count()
161    }
162
163    /// Get the `RoomMember` with the given `user_id`.
164    ///
165    /// Returns `None` if the member was never part of this room, otherwise
166    /// return a `RoomMember` that can be in a joined, RoomState::Invited, left,
167    /// banned state.
168    ///
169    /// Async because it can read from storage.
170    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    /// The current `MemberRoomInfo` for this room.
210    ///
211    /// Async because it can read from storage.
212    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/// A member of a room.
246#[derive(Clone, Debug)]
247pub struct RoomMember {
248    pub(crate) event: Arc<MemberEvent>,
249    // The latest member event sent by the member themselves. Stored in addition
250    // to the latest member event overall to get displayname and avatar from,
251    // which should be ignored on events sent by others.
252    pub(crate) profile: Arc<Option<MinimalRoomMemberEvent>>,
253    // The user's status, taken from their global profile.
254    #[cfg(feature = "unstable-msc4426")]
255    pub(crate) status: Option<StatusProfileField>,
256    // The user's call indicator, taken from their global profile.
257    #[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    /// Build a member from its parts.
268    ///
269    /// `display_name` must be the value returned by
270    /// [`MemberEvent::display_name`] for this `event`.
271    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        // Extract global profile fields, swallowing any deserialization errors.
294        #[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    /// Get the unique user id of this member.
315    pub fn user_id(&self) -> &UserId {
316        self.event.user_id()
317    }
318
319    /// Get the original member event
320    pub fn event(&self) -> &Arc<MemberEvent> {
321        &self.event
322    }
323
324    /// Get the display name of the member if there is one.
325    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    /// Get the name of the member.
334    ///
335    /// This returns either the display name or the local part of the user id if
336    /// the member didn't set a display name.
337    pub fn name(&self) -> &str {
338        if let Some(d) = self.display_name() { d } else { self.user_id().localpart() }
339    }
340
341    /// Get the avatar url of the member, if there is one.
342    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    /// Get the user's status, if any.
351    ///
352    /// This is the user-set status (emoji + text) taken from the user's global
353    /// profile.
354    ///
355    /// Global profiles are only populated when syncing via sliding sync with
356    /// the profiles extension enabled; this is always `None` under sync v2.
357    #[cfg(feature = "unstable-msc4426")]
358    pub fn status(&self) -> Option<&StatusProfileField> {
359        self.status.as_ref()
360    }
361
362    /// Get the user's call indicator, if any.
363    ///
364    /// This indicates the user is currently in a call (and optionally how long
365    /// they've been in it). It is taken from the user's global profile.
366    ///
367    /// Global profiles are only populated when syncing via sliding sync with
368    /// the profiles extension enabled; this is always `None` under sync v2.
369    #[cfg(feature = "unstable-msc4426")]
370    pub fn call(&self) -> Option<&CallProfileField> {
371        self.call.as_ref()
372    }
373
374    /// Get the normalized power level of this member.
375    ///
376    /// The normalized power level depends on the maximum power level that can
377    /// be found in a certain room, positive values that are not `Infinite` are
378    /// always in the range of 0-100.
379    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    /// Get the power level of this member.
394    pub fn power_level(&self) -> UserPowerLevel {
395        self.power_levels.for_user(self.user_id())
396    }
397
398    /// Whether this user can ban other users based on the power levels.
399    ///
400    /// Same as `member.can_do(PowerLevelAction::Ban)`.
401    pub fn can_ban(&self) -> bool {
402        self.can_do_impl(|pls| pls.user_can_ban(self.user_id()))
403    }
404
405    /// Whether this user can invite other users based on the power levels.
406    ///
407    /// Same as `member.can_do(PowerLevelAction::Invite)`.
408    pub fn can_invite(&self) -> bool {
409        self.can_do_impl(|pls| pls.user_can_invite(self.user_id()))
410    }
411
412    /// Whether this user can kick other users based on the power levels.
413    ///
414    /// Same as `member.can_do(PowerLevelAction::Kick)`.
415    pub fn can_kick(&self) -> bool {
416        self.can_do_impl(|pls| pls.user_can_kick(self.user_id()))
417    }
418
419    /// Whether this user can redact their own events based on the power levels.
420    ///
421    /// Same as `member.can_do(PowerLevelAction::RedactOwn)`.
422    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    /// Whether this user can redact events of other users based on the power
427    /// levels.
428    ///
429    /// Same as `member.can_do(PowerLevelAction::RedactOther)`.
430    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    /// Whether this user can send message events based on the power levels.
435    ///
436    /// Same as `member.can_do(PowerLevelAction::SendMessage(msg_type))`.
437    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    /// Whether this user can send state events based on the power levels.
442    ///
443    /// Same as `member.can_do(PowerLevelAction::SendState(state_type))`.
444    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    /// Whether this user can pin or unpin events based on the power levels.
449    pub fn can_pin_or_unpin_event(&self) -> bool {
450        self.can_send_state(StateEventType::RoomPinnedEvents)
451    }
452
453    /// Whether this user can notify everybody in the room by writing `@room` in
454    /// a message.
455    ///
456    /// Same as
457    /// `member. can_do(PowerLevelAction::TriggerNotification(NotificationPowerLevelType::Room))`.
458    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    /// Whether this user can do the given action based on the power levels.
463    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    /// Is the name that the member uses ambiguous in the room.
472    ///
473    /// A name is considered to be ambiguous if at least one other member shares
474    /// the same name.
475    pub fn name_ambiguous(&self) -> bool {
476        self.display_name_ambiguous
477    }
478
479    /// Get the membership state of this member.
480    pub fn membership(&self) -> &MembershipState {
481        self.event.membership()
482    }
483
484    /// Is the room member ignored by the current account user
485    pub fn is_ignored(&self) -> bool {
486        self.is_ignored
487    }
488
489    /// Is the room member a service member.
490    pub fn is_service_member(&self) -> bool {
491        self.is_service_member
492    }
493}
494
495// Information about the room a member is in.
496pub(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/// The kind of room member updates that just happened.
505#[derive(Debug, Clone)]
506pub enum RoomMembersUpdate {
507    /// The whole list room members was reloaded.
508    FullReload,
509    /// A few members were updated, their user ids are included.
510    Partial(BTreeSet<OwnedUserId>),
511}
512
513bitflags! {
514    /// Room membership filter as a bitset.
515    ///
516    /// Note that [`RoomMemberships::empty()`] doesn't filter the results and
517    /// [`RoomMemberships::all()`] filters out unknown memberships.
518    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
519    pub struct RoomMemberships: u16 {
520        /// The member joined the room.
521        const JOIN    = 0b00000001;
522        /// The member was invited to the room.
523        const INVITE  = 0b00000010;
524        /// The member requested to join the room.
525        const KNOCK   = 0b00000100;
526        /// The member left the room.
527        const LEAVE   = 0b00001000;
528        /// The member was banned.
529        const BAN     = 0b00010000;
530
531        /// The member is active in the room (i.e. joined or invited).
532        const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
533    }
534}
535
536impl RoomMemberships {
537    /// Whether the given membership matches this `RoomMemberships`.
538    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    /// Get this `RoomMemberships` as a list of matching [`MembershipState`]s.
556    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
579/// Scale the given `power_level` to a range between 0-100.
580pub 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        // Without a global profile, neither field is set.
666        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        // A global profile carrying both m.status and m.call is extracted into
672        // both fields.
673        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}