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::{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    /// Check if the room has its members fully synced.
48    ///
49    /// Members might be missing if lazy member loading was enabled for the
50    /// sync.
51    ///
52    /// Returns true if no members are missing, false otherwise.
53    pub fn are_members_synced(&self) -> bool {
54        self.info.read().members_synced
55    }
56
57    /// Mark this Room as holding all member information.
58    ///
59    /// Useful in tests if we want to persuade the Room not to sync when asked
60    /// about its members.
61    #[cfg(feature = "testing")]
62    pub fn mark_members_synced(&self) {
63        self.info.update(|info| {
64            info.members_synced = true;
65        });
66    }
67
68    /// Mark this Room as still missing member information.
69    pub fn mark_members_missing(&self) {
70        self.info.update_if(|info| {
71            // notify observable subscribers only if the previous value was 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        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    /// Returns the number of members who have joined or been invited to the
135    /// room.
136    pub fn active_members_count(&self) -> u64 {
137        self.info.read().active_members_count()
138    }
139
140    /// Returns the number of members who have been invited to the room.
141    pub fn invited_members_count(&self) -> u64 {
142        self.info.read().invited_members_count()
143    }
144
145    /// Returns the number of members who have joined the room.
146    pub fn joined_members_count(&self) -> u64 {
147        self.info.read().joined_members_count()
148    }
149
150    /// Get the `RoomMember` with the given `user_id`.
151    ///
152    /// Returns `None` if the member was never part of this room, otherwise
153    /// return a `RoomMember` that can be in a joined, RoomState::Invited, left,
154    /// banned state.
155    ///
156    /// Async because it can read from storage.
157    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    /// The current `MemberRoomInfo` for this room.
203    ///
204    /// Async because it can read from storage.
205    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/// A member of a room.
239#[derive(Clone, Debug)]
240pub struct RoomMember {
241    pub(crate) event: Arc<MemberEvent>,
242    // The latest member event sent by the member themselves.
243    // Stored in addition to the latest member event overall to get displayname
244    // and avatar from, which should be ignored on events sent by others.
245    pub(crate) profile: Arc<Option<MinimalRoomMemberEvent>>,
246    // The user's status, taken from their global profile.
247    #[cfg(feature = "unstable-msc4426")]
248    pub(crate) status: Option<StatusProfileField>,
249    // The user's call indicator, taken from their global profile.
250    #[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        // Extract global profile fields, swallowing any deserialization errors.
286        #[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    /// Get the unique user id of this member.
308    pub fn user_id(&self) -> &UserId {
309        self.event.user_id()
310    }
311
312    /// Get the original member event
313    pub fn event(&self) -> &Arc<MemberEvent> {
314        &self.event
315    }
316
317    /// Get the display name of the member if there is one.
318    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    /// Get the name of the member.
327    ///
328    /// This returns either the display name or the local part of the user id if
329    /// the member didn't set a display name.
330    pub fn name(&self) -> &str {
331        if let Some(d) = self.display_name() { d } else { self.user_id().localpart() }
332    }
333
334    /// Get the avatar url of the member, if there is one.
335    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    /// Get the user's status, if any.
344    ///
345    /// This is the user-set status (emoji + text) taken from the user's global
346    /// profile.
347    ///
348    /// Global profiles are only populated when syncing via sliding sync with
349    /// the profiles extension enabled; this is always `None` under sync v2.
350    #[cfg(feature = "unstable-msc4426")]
351    pub fn status(&self) -> Option<&StatusProfileField> {
352        self.status.as_ref()
353    }
354
355    /// Get the user's call indicator, if any.
356    ///
357    /// This indicates the user is currently in a call (and optionally how long
358    /// they've been in it). It is taken from the user's global profile.
359    ///
360    /// Global profiles are only populated when syncing via sliding sync with
361    /// the profiles extension enabled; this is always `None` under sync v2.
362    #[cfg(feature = "unstable-msc4426")]
363    pub fn call(&self) -> Option<&CallProfileField> {
364        self.call.as_ref()
365    }
366
367    /// Get the normalized power level of this member.
368    ///
369    /// The normalized power level depends on the maximum power level that can
370    /// be found in a certain room, positive values that are not `Infinite` are
371    /// always in the range of 0-100.
372    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    /// Get the power level of this member.
387    pub fn power_level(&self) -> UserPowerLevel {
388        self.power_levels.for_user(self.user_id())
389    }
390
391    /// Whether this user can ban other users based on the power levels.
392    ///
393    /// Same as `member.can_do(PowerLevelAction::Ban)`.
394    pub fn can_ban(&self) -> bool {
395        self.can_do_impl(|pls| pls.user_can_ban(self.user_id()))
396    }
397
398    /// Whether this user can invite other users based on the power levels.
399    ///
400    /// Same as `member.can_do(PowerLevelAction::Invite)`.
401    pub fn can_invite(&self) -> bool {
402        self.can_do_impl(|pls| pls.user_can_invite(self.user_id()))
403    }
404
405    /// Whether this user can kick other users based on the power levels.
406    ///
407    /// Same as `member.can_do(PowerLevelAction::Kick)`.
408    pub fn can_kick(&self) -> bool {
409        self.can_do_impl(|pls| pls.user_can_kick(self.user_id()))
410    }
411
412    /// Whether this user can redact their own events based on the power levels.
413    ///
414    /// Same as `member.can_do(PowerLevelAction::RedactOwn)`.
415    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    /// Whether this user can redact events of other users based on the power
420    /// levels.
421    ///
422    /// Same as `member.can_do(PowerLevelAction::RedactOther)`.
423    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    /// Whether this user can send message events based on the power levels.
428    ///
429    /// Same as `member.can_do(PowerLevelAction::SendMessage(msg_type))`.
430    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    /// Whether this user can send state events based on the power levels.
435    ///
436    /// Same as `member.can_do(PowerLevelAction::SendState(state_type))`.
437    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    /// Whether this user can pin or unpin events based on the power levels.
442    pub fn can_pin_or_unpin_event(&self) -> bool {
443        self.can_send_state(StateEventType::RoomPinnedEvents)
444    }
445
446    /// Whether this user can notify everybody in the room by writing `@room` in
447    /// a message.
448    ///
449    /// Same as `member.
450    /// can_do(PowerLevelAction::TriggerNotification(NotificationPowerLevelType::Room))`.
451    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    /// Whether this user can do the given action based on the power
456    /// levels.
457    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    /// Is the name that the member uses ambiguous in the room.
466    ///
467    /// A name is considered to be ambiguous if at least one other member shares
468    /// the same name.
469    pub fn name_ambiguous(&self) -> bool {
470        self.display_name_ambiguous
471    }
472
473    /// Get the membership state of this member.
474    pub fn membership(&self) -> &MembershipState {
475        self.event.membership()
476    }
477
478    /// Is the room member ignored by the current account user
479    pub fn is_ignored(&self) -> bool {
480        self.is_ignored
481    }
482
483    /// Is the room member a service member.
484    pub fn is_service_member(&self) -> bool {
485        self.is_service_member
486    }
487}
488
489// Information about the room a member is in.
490pub(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/// The kind of room member updates that just happened.
499#[derive(Debug, Clone)]
500pub enum RoomMembersUpdate {
501    /// The whole list room members was reloaded.
502    FullReload,
503    /// A few members were updated, their user ids are included.
504    Partial(BTreeSet<OwnedUserId>),
505}
506
507bitflags! {
508    /// Room membership filter as a bitset.
509    ///
510    /// Note that [`RoomMemberships::empty()`] doesn't filter the results and
511    /// [`RoomMemberships::all()`] filters out unknown memberships.
512    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
513    pub struct RoomMemberships: u16 {
514        /// The member joined the room.
515        const JOIN    = 0b00000001;
516        /// The member was invited to the room.
517        const INVITE  = 0b00000010;
518        /// The member requested to join the room.
519        const KNOCK   = 0b00000100;
520        /// The member left the room.
521        const LEAVE   = 0b00001000;
522        /// The member was banned.
523        const BAN     = 0b00010000;
524
525        /// The member is active in the room (i.e. joined or invited).
526        const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
527    }
528}
529
530impl RoomMemberships {
531    /// Whether the given membership matches this `RoomMemberships`.
532    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    /// Get this `RoomMemberships` as a list of matching [`MembershipState`]s.
550    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
573/// Scale the given `power_level` to a range between 0-100.
574pub 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        // Without a global profile, neither field is set.
660        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        // A global profile carrying both m.status and m.call is extracted into
665        // both fields.
666        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}