Skip to main content

matrix_sdk/room/
power_levels.rs

1//! Power level configuration types used in [the `room` module][super].
2
3use std::collections::HashMap;
4
5use ruma::{
6    OwnedUserId,
7    events::{
8        MessageLikeEventType, StateEventType,
9        room::power_levels::{
10            PossiblyRedactedRoomPowerLevelsEventContent, RoomPowerLevels,
11            RoomPowerLevelsEventContent,
12        },
13    },
14};
15
16use crate::Result;
17
18/// A set of common power levels required for various operations within a room,
19/// that can be applied as a single operation. When updating these
20/// settings, any levels that are `None` will remain unchanged.
21#[derive(Debug)]
22#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
23pub struct RoomPowerLevelChanges {
24    // Actions
25    /// The level required to ban a user.
26    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
27    pub ban: Option<i64>,
28    /// The level required to invite a user.
29    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
30    pub invite: Option<i64>,
31    /// The level required to kick a user.
32    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
33    pub kick: Option<i64>,
34    /// The level required to redact an event.
35    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
36    pub redact: Option<i64>,
37
38    // Events
39    /// The default level required to send message events.
40    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
41    pub events_default: Option<i64>,
42    /// The default level required to send state events.
43    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
44    pub state_default: Option<i64>,
45    /// The default power level for every user in the room.
46    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
47    pub users_default: Option<i64>,
48    /// The level required to change the room's name.
49    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
50    pub room_name: Option<i64>,
51    /// The level required to change the room's avatar.
52    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
53    pub room_avatar: Option<i64>,
54    /// The level required to change the room's topic.
55    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
56    pub room_topic: Option<i64>,
57    /// The level required to change the space's children.
58    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
59    pub space_child: Option<i64>,
60    /// The level required to send a beacon (live location) message event.
61    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
62    pub beacon: Option<i64>,
63    /// The level required to send a beacon info state event.
64    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
65    pub beacon_info: Option<i64>,
66}
67
68impl RoomPowerLevelChanges {
69    /// Constructs an empty set of `RoomPowerLevelChanges`.
70    pub fn new() -> Self {
71        Self {
72            ban: None,
73            invite: None,
74            kick: None,
75            redact: None,
76            events_default: None,
77            state_default: None,
78            users_default: None,
79            room_name: None,
80            room_avatar: None,
81            room_topic: None,
82            space_child: None,
83            beacon: None,
84            beacon_info: None,
85        }
86    }
87}
88
89impl Default for RoomPowerLevelChanges {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl From<RoomPowerLevels> for RoomPowerLevelChanges {
96    fn from(value: RoomPowerLevels) -> Self {
97        Self {
98            ban: Some(value.ban.into()),
99            invite: Some(value.invite.into()),
100            kick: Some(value.kick.into()),
101            redact: Some(value.redact.into()),
102            events_default: Some(value.events_default.into()),
103            state_default: Some(value.state_default.into()),
104            users_default: Some(value.users_default.into()),
105            room_name: value
106                .events
107                .get(&StateEventType::RoomName.into())
108                .map(|v| (*v).into())
109                .or(Some(value.state_default.into())),
110            room_avatar: value
111                .events
112                .get(&StateEventType::RoomAvatar.into())
113                .map(|v| (*v).into())
114                .or(Some(value.state_default.into())),
115            room_topic: value
116                .events
117                .get(&StateEventType::RoomTopic.into())
118                .map(|v| (*v).into())
119                .or(Some(value.state_default.into())),
120            space_child: value
121                .events
122                .get(&StateEventType::SpaceChild.into())
123                .map(|v| (*v).into())
124                .or(Some(value.state_default.into())),
125            beacon: value
126                .events
127                .get(&MessageLikeEventType::Beacon.into())
128                .map(|v| (*v).into())
129                .or(Some(value.events_default.into())),
130            beacon_info: value
131                .events
132                .get(&StateEventType::BeaconInfo.into())
133                .map(|v| (*v).into())
134                .or(Some(value.state_default.into())),
135        }
136    }
137}
138
139pub(crate) trait RoomPowerLevelsExt {
140    /// Applies the updated settings to the power levels. Any levels that are
141    /// `None` will remain unchanged. Unlike with members, we don't remove the
142    /// event if the new level matches the default as this could result in
143    /// unintended privileges when updating the default power level in
144    /// isolation of the others.
145    fn apply(&mut self, settings: RoomPowerLevelChanges) -> Result<()>;
146}
147
148impl RoomPowerLevelsExt for RoomPowerLevels {
149    fn apply(&mut self, settings: RoomPowerLevelChanges) -> Result<()> {
150        if let Some(ban) = settings.ban {
151            self.ban = ban.try_into()?;
152        }
153        if let Some(invite) = settings.invite {
154            self.invite = invite.try_into()?;
155        }
156        if let Some(kick) = settings.kick {
157            self.kick = kick.try_into()?;
158        }
159        if let Some(redact) = settings.redact {
160            self.redact = redact.try_into()?;
161        }
162        if let Some(events_default) = settings.events_default {
163            self.events_default = events_default.try_into()?;
164        }
165        if let Some(state_default) = settings.state_default {
166            self.state_default = state_default.try_into()?;
167        }
168        if let Some(users_default) = settings.users_default {
169            self.users_default = users_default.try_into()?;
170        }
171        if let Some(room_name) = settings.room_name {
172            self.events.insert(StateEventType::RoomName.into(), room_name.try_into()?);
173        }
174        if let Some(room_avatar) = settings.room_avatar {
175            self.events.insert(StateEventType::RoomAvatar.into(), room_avatar.try_into()?);
176        }
177        if let Some(room_topic) = settings.room_topic {
178            self.events.insert(StateEventType::RoomTopic.into(), room_topic.try_into()?);
179        }
180        if let Some(space_child) = settings.space_child {
181            self.events.insert(StateEventType::SpaceChild.into(), space_child.try_into()?);
182        }
183        if let Some(beacon) = settings.beacon {
184            self.events.insert(MessageLikeEventType::Beacon.into(), beacon.try_into()?);
185        }
186        if let Some(beacon_info) = settings.beacon_info {
187            self.events.insert(StateEventType::BeaconInfo.into(), beacon_info.try_into()?);
188        }
189
190        Ok(())
191    }
192}
193
194impl From<js_int::TryFromIntError> for crate::error::Error {
195    fn from(e: js_int::TryFromIntError) -> Self {
196        crate::error::Error::UnknownError(Box::new(e))
197    }
198}
199
200/// Checks for changes in the power levels of users in a room based on a new
201/// event.
202pub fn power_level_user_changes(
203    content: &RoomPowerLevelsEventContent,
204    prev_content: &Option<PossiblyRedactedRoomPowerLevelsEventContent>,
205) -> HashMap<OwnedUserId, i64> {
206    let Some(prev_content) = prev_content.as_ref() else {
207        return Default::default();
208    };
209
210    let mut changes = HashMap::new();
211    let mut prev_users = prev_content.users.clone();
212    let new_users = content.users.clone();
213
214    // If a user is in the new power levels, but not in the old ones, or if the
215    // power level has changed, add them to the changes.
216    for (user_id, power_level) in new_users {
217        let prev_power_level = prev_users.remove(&user_id).unwrap_or(prev_content.users_default);
218        if power_level != prev_power_level {
219            changes.insert(user_id, power_level.into());
220        }
221    }
222
223    // Any remaining users from the old power levels have had their power level set
224    // back to default.
225    for (user_id, power_level) in prev_users {
226        if power_level != content.users_default {
227            changes.insert(user_id, content.users_default.into());
228        }
229    }
230
231    changes
232}
233
234#[cfg(test)]
235mod tests {
236    use std::collections::BTreeMap;
237
238    use ruma::{
239        int, power_levels::NotificationPowerLevels, room_version_rules::AuthorizationRules,
240    };
241
242    use super::*;
243
244    #[test]
245    fn test_apply_actions() {
246        // Given a set of power levels and some settings that only change the
247        // actions.
248        let mut power_levels = default_power_levels();
249
250        let new_level = int!(100);
251        let settings = RoomPowerLevelChanges {
252            ban: Some(new_level.into()),
253            invite: Some(new_level.into()),
254            kick: Some(new_level.into()),
255            redact: Some(new_level.into()),
256            events_default: None,
257            state_default: None,
258            users_default: None,
259            room_name: None,
260            room_avatar: None,
261            room_topic: None,
262            space_child: None,
263            beacon: None,
264            beacon_info: None,
265        };
266
267        // When applying the settings to the power levels.
268        let original_levels = power_levels.clone();
269        power_levels.apply(settings).unwrap();
270
271        // Then the levels for the actions should be updated.
272        assert_eq!(power_levels.ban, new_level);
273        assert_eq!(power_levels.invite, new_level);
274        assert_eq!(power_levels.kick, new_level);
275        assert_eq!(power_levels.redact, new_level);
276        // And the rest should remain unchanged.
277        assert_eq!(power_levels.events_default, original_levels.events_default);
278        assert_eq!(power_levels.state_default, original_levels.state_default);
279        assert_eq!(power_levels.users_default, original_levels.users_default);
280        assert_eq!(power_levels.events, original_levels.events);
281    }
282
283    #[test]
284    fn test_apply_room_settings() {
285        // Given a set of power levels and some settings that only change the specific
286        // state event levels.
287        let mut power_levels = default_power_levels();
288
289        let new_level = int!(100);
290        let settings = RoomPowerLevelChanges {
291            ban: None,
292            invite: None,
293            kick: None,
294            redact: None,
295            events_default: None,
296            state_default: None,
297            users_default: None,
298            room_name: Some(new_level.into()),
299            room_avatar: Some(new_level.into()),
300            room_topic: Some(new_level.into()),
301            space_child: Some(new_level.into()),
302            beacon: None,
303            beacon_info: None,
304        };
305
306        // When applying the settings to the power levels.
307        let original_levels = power_levels.clone();
308        power_levels.apply(settings).unwrap();
309
310        // Then levels for the necessary state events should be added.
311        assert_eq!(
312            power_levels.events,
313            BTreeMap::from_iter(vec![
314                (StateEventType::RoomName.into(), new_level),
315                (StateEventType::RoomAvatar.into(), new_level),
316                (StateEventType::RoomTopic.into(), new_level),
317                (StateEventType::SpaceChild.into(), new_level),
318            ])
319        );
320        // And the rest should remain unchanged.
321        assert_eq!(power_levels.ban, original_levels.ban);
322        assert_eq!(power_levels.invite, original_levels.invite);
323        assert_eq!(power_levels.kick, original_levels.kick);
324        assert_eq!(power_levels.redact, original_levels.redact);
325        assert_eq!(power_levels.events_default, original_levels.events_default);
326        assert_eq!(power_levels.state_default, original_levels.state_default);
327        assert_eq!(power_levels.users_default, original_levels.users_default);
328    }
329
330    #[test]
331    fn test_apply_state_event_to_default() {
332        // Given a set of power levels and some settings that change the room name level
333        // back to the default level.
334        let original_level = int!(100);
335        let mut power_levels = default_power_levels();
336        power_levels.events = BTreeMap::from_iter(vec![
337            (StateEventType::RoomName.into(), original_level),
338            (StateEventType::RoomAvatar.into(), original_level),
339            (StateEventType::RoomTopic.into(), original_level),
340            (StateEventType::SpaceChild.into(), original_level),
341        ]);
342
343        let settings = RoomPowerLevelChanges {
344            ban: None,
345            invite: None,
346            kick: None,
347            redact: None,
348            events_default: None,
349            state_default: None,
350            users_default: None,
351            room_name: Some(power_levels.state_default.into()),
352            room_avatar: None,
353            room_topic: None,
354            space_child: None,
355            beacon: None,
356            beacon_info: None,
357        };
358
359        // When applying the settings to the power levels.
360        let original_levels = power_levels.clone();
361        power_levels.apply(settings).unwrap();
362
363        // Then the room name level should be updated (but not removed) without
364        // affecting any other state events.
365        assert_eq!(
366            power_levels.events,
367            BTreeMap::from_iter(vec![
368                (StateEventType::RoomName.into(), power_levels.state_default),
369                (StateEventType::RoomAvatar.into(), original_level),
370                (StateEventType::RoomTopic.into(), original_level),
371                (StateEventType::SpaceChild.into(), original_level),
372            ])
373        );
374        // And the rest should remain unchanged.
375        assert_eq!(power_levels.ban, original_levels.ban);
376        assert_eq!(power_levels.invite, original_levels.invite);
377        assert_eq!(power_levels.kick, original_levels.kick);
378        assert_eq!(power_levels.redact, original_levels.redact);
379        assert_eq!(power_levels.events_default, original_levels.events_default);
380        assert_eq!(power_levels.state_default, original_levels.state_default);
381        assert_eq!(power_levels.users_default, original_levels.users_default);
382    }
383
384    #[test]
385    fn test_apply_beacon_settings() {
386        // Given a set of power levels and some settings that only change the beacon
387        // and beacon_info event levels.
388        let mut power_levels = default_power_levels();
389
390        let new_level = int!(25);
391        let settings = RoomPowerLevelChanges {
392            ban: None,
393            invite: None,
394            kick: None,
395            redact: None,
396            events_default: None,
397            state_default: None,
398            users_default: None,
399            room_name: None,
400            room_avatar: None,
401            room_topic: None,
402            space_child: None,
403            beacon: Some(new_level.into()),
404            beacon_info: Some(new_level.into()),
405        };
406
407        // When applying the settings to the power levels.
408        let original_levels = power_levels.clone();
409        power_levels.apply(settings).unwrap();
410
411        // Then levels for the beacon events should be added.
412        assert_eq!(
413            power_levels.events.get(&MessageLikeEventType::Beacon.into()).copied(),
414            Some(new_level)
415        );
416        assert_eq!(
417            power_levels.events.get(&StateEventType::BeaconInfo.into()).copied(),
418            Some(new_level)
419        );
420        // And the rest should remain unchanged.
421        assert_eq!(power_levels.ban, original_levels.ban);
422        assert_eq!(power_levels.invite, original_levels.invite);
423        assert_eq!(power_levels.kick, original_levels.kick);
424        assert_eq!(power_levels.redact, original_levels.redact);
425        assert_eq!(power_levels.events_default, original_levels.events_default);
426        assert_eq!(power_levels.state_default, original_levels.state_default);
427        assert_eq!(power_levels.users_default, original_levels.users_default);
428    }
429
430    #[test]
431    fn test_user_power_level_changes_add_mod() {
432        // Given a set of power levels and a new set of power levels that adds a new
433        // moderator.
434        let prev_content = default_power_levels_event_content();
435        let mut content = prev_content.clone();
436        content.users.insert(OwnedUserId::try_from("@charlie:example.com").unwrap(), int!(50));
437
438        // When calculating the changes.
439        let changes = power_level_user_changes(&content, &Some(prev_content));
440
441        // Then the changes should reflect the new moderator.
442        assert_eq!(changes.len(), 1);
443        assert_eq!(changes.get(&OwnedUserId::try_from("@charlie:example.com").unwrap()), Some(&50));
444    }
445
446    #[test]
447    fn test_user_power_level_changes_remove_mod() {
448        // Given a set of power levels and a new set of power levels that removes a
449        // moderator.
450        let prev_content = default_power_levels_event_content();
451        let mut content = prev_content.clone();
452        content.users.remove(&OwnedUserId::try_from("@bob:example.com").unwrap());
453
454        // When calculating the changes.
455        let changes = power_level_user_changes(&content, &Some(prev_content));
456
457        // Then the changes should reflect the removed moderator.
458        assert_eq!(changes.len(), 1);
459        assert_eq!(changes.get(&OwnedUserId::try_from("@bob:example.com").unwrap()), Some(&0));
460    }
461
462    #[test]
463    fn test_user_power_level_changes_change_mod() {
464        // Given a set of power levels and a new set of power levels that changes a
465        // moderator to an admin.
466        let prev_content = default_power_levels_event_content();
467        let mut content = prev_content.clone();
468        content.users.insert(OwnedUserId::try_from("@bob:example.com").unwrap(), int!(100));
469
470        // When calculating the changes.
471        let changes = power_level_user_changes(&content, &Some(prev_content));
472
473        // Then the changes should reflect the new admin.
474        assert_eq!(changes.len(), 1);
475        assert_eq!(changes.get(&OwnedUserId::try_from("@bob:example.com").unwrap()), Some(&100));
476    }
477
478    #[test]
479    fn test_user_power_level_changes_new_default() {
480        // Given a set of power levels and a new set of power levels that changes the
481        // default user power level to moderator and removes the only moderator.
482        let prev_content = default_power_levels_event_content();
483        let mut content = prev_content.clone();
484        content.users_default = int!(50);
485        content.users.remove(&OwnedUserId::try_from("@bob:example.com").unwrap());
486
487        // When calculating the changes.
488        let changes = power_level_user_changes(&content, &Some(prev_content));
489
490        // Then there should be no changes.
491        assert!(changes.is_empty());
492    }
493
494    #[test]
495    fn test_user_power_level_changes_no_change() {
496        // Given a set of power levels and a new set of power levels that's the same.
497        let prev_content = default_power_levels_event_content();
498        let content = prev_content.clone();
499
500        // When calculating the changes.
501        let changes = power_level_user_changes(&content, &Some(prev_content));
502
503        // Then there should be no changes.
504        assert!(changes.is_empty());
505    }
506
507    #[test]
508    fn test_user_power_level_changes_other_properties() {
509        // Given a set of power levels and a new set of power levels with changes that
510        // don't include the user power levels.
511        let prev_content = default_power_levels_event_content();
512        let mut content = prev_content.clone();
513        content.events_default = int!(100);
514
515        // When calculating the changes.
516        let changes = power_level_user_changes(&content, &Some(prev_content));
517
518        // Then there should be no changes.
519        assert!(changes.is_empty());
520    }
521
522    fn default_power_levels() -> RoomPowerLevels {
523        RoomPowerLevels::new(
524            default_power_levels_event_content().into(),
525            &AuthorizationRules::V1,
526            [],
527        )
528    }
529
530    fn default_power_levels_event_content() -> RoomPowerLevelsEventContent {
531        let mut content = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
532        content.ban = int!(50);
533        content.invite = int!(50);
534        content.kick = int!(50);
535        content.redact = int!(50);
536        content.events_default = int!(0);
537        content.state_default = int!(50);
538        content.users_default = int!(0);
539        content.users = BTreeMap::from_iter(vec![
540            (OwnedUserId::try_from("@alice:example.com").unwrap(), int!(100)),
541            (OwnedUserId::try_from("@bob:example.com").unwrap(), int!(50)),
542        ]);
543        content.notifications = NotificationPowerLevels::default();
544        content
545    }
546}