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