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        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}
61
62impl RoomPowerLevelChanges {
63    /// Constructs an empty set of `RoomPowerLevelChanges`.
64    pub fn new() -> Self {
65        Self {
66            ban: None,
67            invite: None,
68            kick: None,
69            redact: None,
70            events_default: None,
71            state_default: None,
72            users_default: None,
73            room_name: None,
74            room_avatar: None,
75            room_topic: None,
76            space_child: None,
77        }
78    }
79}
80
81impl Default for RoomPowerLevelChanges {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87impl From<RoomPowerLevels> for RoomPowerLevelChanges {
88    fn from(value: RoomPowerLevels) -> Self {
89        Self {
90            ban: Some(value.ban.into()),
91            invite: Some(value.invite.into()),
92            kick: Some(value.kick.into()),
93            redact: Some(value.redact.into()),
94            events_default: Some(value.events_default.into()),
95            state_default: Some(value.state_default.into()),
96            users_default: Some(value.users_default.into()),
97            room_name: value
98                .events
99                .get(&StateEventType::RoomName.into())
100                .map(|v| (*v).into())
101                .or(Some(value.state_default.into())),
102            room_avatar: value
103                .events
104                .get(&StateEventType::RoomAvatar.into())
105                .map(|v| (*v).into())
106                .or(Some(value.state_default.into())),
107            room_topic: value
108                .events
109                .get(&StateEventType::RoomTopic.into())
110                .map(|v| (*v).into())
111                .or(Some(value.state_default.into())),
112            space_child: value
113                .events
114                .get(&StateEventType::SpaceChild.into())
115                .map(|v| (*v).into())
116                .or(Some(value.state_default.into())),
117        }
118    }
119}
120
121pub(crate) trait RoomPowerLevelsExt {
122    /// Applies the updated settings to the power levels. Any levels that are
123    /// `None` will remain unchanged. Unlike with members, we don't remove the
124    /// event if the new level matches the default as this could result in
125    /// unintended privileges when updating the default power level in
126    /// isolation of the others.
127    fn apply(&mut self, settings: RoomPowerLevelChanges) -> Result<()>;
128}
129
130impl RoomPowerLevelsExt for RoomPowerLevels {
131    fn apply(&mut self, settings: RoomPowerLevelChanges) -> Result<()> {
132        if let Some(ban) = settings.ban {
133            self.ban = ban.try_into()?;
134        }
135        if let Some(invite) = settings.invite {
136            self.invite = invite.try_into()?;
137        }
138        if let Some(kick) = settings.kick {
139            self.kick = kick.try_into()?;
140        }
141        if let Some(redact) = settings.redact {
142            self.redact = redact.try_into()?;
143        }
144        if let Some(events_default) = settings.events_default {
145            self.events_default = events_default.try_into()?;
146        }
147        if let Some(state_default) = settings.state_default {
148            self.state_default = state_default.try_into()?;
149        }
150        if let Some(users_default) = settings.users_default {
151            self.users_default = users_default.try_into()?;
152        }
153        if let Some(room_name) = settings.room_name {
154            self.events.insert(StateEventType::RoomName.into(), room_name.try_into()?);
155        }
156        if let Some(room_avatar) = settings.room_avatar {
157            self.events.insert(StateEventType::RoomAvatar.into(), room_avatar.try_into()?);
158        }
159        if let Some(room_topic) = settings.room_topic {
160            self.events.insert(StateEventType::RoomTopic.into(), room_topic.try_into()?);
161        }
162        if let Some(space_child) = settings.space_child {
163            self.events.insert(StateEventType::SpaceChild.into(), space_child.try_into()?);
164        }
165
166        Ok(())
167    }
168}
169
170impl From<js_int::TryFromIntError> for crate::error::Error {
171    fn from(e: js_int::TryFromIntError) -> Self {
172        crate::error::Error::UnknownError(Box::new(e))
173    }
174}
175
176/// Checks for changes in the power levels of users in a room based on a new
177/// event.
178pub fn power_level_user_changes(
179    content: &RoomPowerLevelsEventContent,
180    prev_content: &Option<PossiblyRedactedRoomPowerLevelsEventContent>,
181) -> HashMap<OwnedUserId, i64> {
182    let Some(prev_content) = prev_content.as_ref() else {
183        return Default::default();
184    };
185
186    let mut changes = HashMap::new();
187    let mut prev_users = prev_content.users.clone();
188    let new_users = content.users.clone();
189
190    // If a user is in the new power levels, but not in the old ones, or if the
191    // power level has changed, add them to the changes.
192    for (user_id, power_level) in new_users {
193        let prev_power_level = prev_users.remove(&user_id).unwrap_or(prev_content.users_default);
194        if power_level != prev_power_level {
195            changes.insert(user_id, power_level.into());
196        }
197    }
198
199    // Any remaining users from the old power levels have had their power level set
200    // back to default.
201    for (user_id, power_level) in prev_users {
202        if power_level != content.users_default {
203            changes.insert(user_id, content.users_default.into());
204        }
205    }
206
207    changes
208}
209
210#[cfg(test)]
211mod tests {
212    use std::collections::BTreeMap;
213
214    use ruma::{
215        int, power_levels::NotificationPowerLevels, room_version_rules::AuthorizationRules,
216    };
217
218    use super::*;
219
220    #[test]
221    fn test_apply_actions() {
222        // Given a set of power levels and some settings that only change the
223        // actions.
224        let mut power_levels = default_power_levels();
225
226        let new_level = int!(100);
227        let settings = RoomPowerLevelChanges {
228            ban: Some(new_level.into()),
229            invite: Some(new_level.into()),
230            kick: Some(new_level.into()),
231            redact: Some(new_level.into()),
232            events_default: None,
233            state_default: None,
234            users_default: None,
235            room_name: None,
236            room_avatar: None,
237            room_topic: None,
238            space_child: None,
239        };
240
241        // When applying the settings to the power levels.
242        let original_levels = power_levels.clone();
243        power_levels.apply(settings).unwrap();
244
245        // Then the levels for the actions should be updated.
246        assert_eq!(power_levels.ban, new_level);
247        assert_eq!(power_levels.invite, new_level);
248        assert_eq!(power_levels.kick, new_level);
249        assert_eq!(power_levels.redact, new_level);
250        // And the rest should remain unchanged.
251        assert_eq!(power_levels.events_default, original_levels.events_default);
252        assert_eq!(power_levels.state_default, original_levels.state_default);
253        assert_eq!(power_levels.users_default, original_levels.users_default);
254        assert_eq!(power_levels.events, original_levels.events);
255    }
256
257    #[test]
258    fn test_apply_room_settings() {
259        // Given a set of power levels and some settings that only change the specific
260        // state event levels.
261        let mut power_levels = default_power_levels();
262
263        let new_level = int!(100);
264        let settings = RoomPowerLevelChanges {
265            ban: None,
266            invite: None,
267            kick: None,
268            redact: None,
269            events_default: None,
270            state_default: None,
271            users_default: None,
272            room_name: Some(new_level.into()),
273            room_avatar: Some(new_level.into()),
274            room_topic: Some(new_level.into()),
275            space_child: Some(new_level.into()),
276        };
277
278        // When applying the settings to the power levels.
279        let original_levels = power_levels.clone();
280        power_levels.apply(settings).unwrap();
281
282        // Then levels for the necessary state events should be added.
283        assert_eq!(
284            power_levels.events,
285            BTreeMap::from_iter(vec![
286                (StateEventType::RoomName.into(), new_level),
287                (StateEventType::RoomAvatar.into(), new_level),
288                (StateEventType::RoomTopic.into(), new_level),
289                (StateEventType::SpaceChild.into(), new_level),
290            ])
291        );
292        // And the rest should remain unchanged.
293        assert_eq!(power_levels.ban, original_levels.ban);
294        assert_eq!(power_levels.invite, original_levels.invite);
295        assert_eq!(power_levels.kick, original_levels.kick);
296        assert_eq!(power_levels.redact, original_levels.redact);
297        assert_eq!(power_levels.events_default, original_levels.events_default);
298        assert_eq!(power_levels.state_default, original_levels.state_default);
299        assert_eq!(power_levels.users_default, original_levels.users_default);
300    }
301
302    #[test]
303    fn test_apply_state_event_to_default() {
304        // Given a set of power levels and some settings that change the room name level
305        // back to the default level.
306        let original_level = int!(100);
307        let mut power_levels = default_power_levels();
308        power_levels.events = BTreeMap::from_iter(vec![
309            (StateEventType::RoomName.into(), original_level),
310            (StateEventType::RoomAvatar.into(), original_level),
311            (StateEventType::RoomTopic.into(), original_level),
312            (StateEventType::SpaceChild.into(), original_level),
313        ]);
314
315        let settings = RoomPowerLevelChanges {
316            ban: None,
317            invite: None,
318            kick: None,
319            redact: None,
320            events_default: None,
321            state_default: None,
322            users_default: None,
323            room_name: Some(power_levels.state_default.into()),
324            room_avatar: None,
325            room_topic: None,
326            space_child: None,
327        };
328
329        // When applying the settings to the power levels.
330        let original_levels = power_levels.clone();
331        power_levels.apply(settings).unwrap();
332
333        // Then the room name level should be updated (but not removed) without
334        // affecting any other state events.
335        assert_eq!(
336            power_levels.events,
337            BTreeMap::from_iter(vec![
338                (StateEventType::RoomName.into(), power_levels.state_default),
339                (StateEventType::RoomAvatar.into(), original_level),
340                (StateEventType::RoomTopic.into(), original_level),
341                (StateEventType::SpaceChild.into(), original_level),
342            ])
343        );
344        // And the rest should remain unchanged.
345        assert_eq!(power_levels.ban, original_levels.ban);
346        assert_eq!(power_levels.invite, original_levels.invite);
347        assert_eq!(power_levels.kick, original_levels.kick);
348        assert_eq!(power_levels.redact, original_levels.redact);
349        assert_eq!(power_levels.events_default, original_levels.events_default);
350        assert_eq!(power_levels.state_default, original_levels.state_default);
351        assert_eq!(power_levels.users_default, original_levels.users_default);
352    }
353
354    #[test]
355    fn test_user_power_level_changes_add_mod() {
356        // Given a set of power levels and a new set of power levels that adds a new
357        // moderator.
358        let prev_content = default_power_levels_event_content();
359        let mut content = prev_content.clone();
360        content.users.insert(OwnedUserId::try_from("@charlie:example.com").unwrap(), int!(50));
361
362        // When calculating the changes.
363        let changes = power_level_user_changes(&content, &Some(prev_content));
364
365        // Then the changes should reflect the new moderator.
366        assert_eq!(changes.len(), 1);
367        assert_eq!(changes.get(&OwnedUserId::try_from("@charlie:example.com").unwrap()), Some(&50));
368    }
369
370    #[test]
371    fn test_user_power_level_changes_remove_mod() {
372        // Given a set of power levels and a new set of power levels that removes a
373        // moderator.
374        let prev_content = default_power_levels_event_content();
375        let mut content = prev_content.clone();
376        content.users.remove(&OwnedUserId::try_from("@bob:example.com").unwrap());
377
378        // When calculating the changes.
379        let changes = power_level_user_changes(&content, &Some(prev_content));
380
381        // Then the changes should reflect the removed moderator.
382        assert_eq!(changes.len(), 1);
383        assert_eq!(changes.get(&OwnedUserId::try_from("@bob:example.com").unwrap()), Some(&0));
384    }
385
386    #[test]
387    fn test_user_power_level_changes_change_mod() {
388        // Given a set of power levels and a new set of power levels that changes a
389        // moderator to an admin.
390        let prev_content = default_power_levels_event_content();
391        let mut content = prev_content.clone();
392        content.users.insert(OwnedUserId::try_from("@bob:example.com").unwrap(), int!(100));
393
394        // When calculating the changes.
395        let changes = power_level_user_changes(&content, &Some(prev_content));
396
397        // Then the changes should reflect the new admin.
398        assert_eq!(changes.len(), 1);
399        assert_eq!(changes.get(&OwnedUserId::try_from("@bob:example.com").unwrap()), Some(&100));
400    }
401
402    #[test]
403    fn test_user_power_level_changes_new_default() {
404        // Given a set of power levels and a new set of power levels that changes the
405        // default user power level to moderator and removes the only moderator.
406        let prev_content = default_power_levels_event_content();
407        let mut content = prev_content.clone();
408        content.users_default = int!(50);
409        content.users.remove(&OwnedUserId::try_from("@bob:example.com").unwrap());
410
411        // When calculating the changes.
412        let changes = power_level_user_changes(&content, &Some(prev_content));
413
414        // Then there should be no changes.
415        assert!(changes.is_empty());
416    }
417
418    #[test]
419    fn test_user_power_level_changes_no_change() {
420        // Given a set of power levels and a new set of power levels that's the same.
421        let prev_content = default_power_levels_event_content();
422        let content = prev_content.clone();
423
424        // When calculating the changes.
425        let changes = power_level_user_changes(&content, &Some(prev_content));
426
427        // Then there should be no changes.
428        assert!(changes.is_empty());
429    }
430
431    #[test]
432    fn test_user_power_level_changes_other_properties() {
433        // Given a set of power levels and a new set of power levels with changes that
434        // don't include the user power levels.
435        let prev_content = default_power_levels_event_content();
436        let mut content = prev_content.clone();
437        content.events_default = int!(100);
438
439        // When calculating the changes.
440        let changes = power_level_user_changes(&content, &Some(prev_content));
441
442        // Then there should be no changes.
443        assert!(changes.is_empty());
444    }
445
446    fn default_power_levels() -> RoomPowerLevels {
447        RoomPowerLevels::new(
448            default_power_levels_event_content().into(),
449            &AuthorizationRules::V1,
450            [],
451        )
452    }
453
454    fn default_power_levels_event_content() -> RoomPowerLevelsEventContent {
455        let mut content = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
456        content.ban = int!(50);
457        content.invite = int!(50);
458        content.kick = int!(50);
459        content.redact = int!(50);
460        content.events_default = int!(0);
461        content.state_default = int!(50);
462        content.users_default = int!(0);
463        content.users = BTreeMap::from_iter(vec![
464            (OwnedUserId::try_from("@alice:example.com").unwrap(), int!(100)),
465            (OwnedUserId::try_from("@bob:example.com").unwrap(), int!(50)),
466        ]);
467        content.notifications = NotificationPowerLevels::default();
468        content
469    }
470}