use std::{cmp::max, collections::BTreeMap};
use js_int::{int, Int};
use ruma_common::{
power_levels::{default_power_level, NotificationPowerLevels},
push::PushConditionPowerLevelsCtx,
OwnedUserId, RoomVersionId, UserId,
};
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
use crate::{
EmptyStateKey, EventContent, MessageLikeEventType, RedactContent, RedactedStateEventContent,
StateEventType, StaticEventContent, TimelineEventType,
};
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.room.power_levels", kind = State, state_key_type = EmptyStateKey, custom_redacted)]
pub struct RoomPowerLevelsEventContent {
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub ban: Int,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "ruma_common::serde::btreemap_deserialize_v1_powerlevel_values"
)]
pub events: BTreeMap<TimelineEventType, Int>,
#[serde(
default,
skip_serializing_if = "ruma_common::serde::is_default",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub events_default: Int,
#[serde(
default,
skip_serializing_if = "ruma_common::serde::is_default",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub invite: Int,
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub kick: Int,
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub redact: Int,
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub state_default: Int,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "ruma_common::serde::btreemap_deserialize_v1_powerlevel_values"
)]
pub users: BTreeMap<OwnedUserId, Int>,
#[serde(
default,
skip_serializing_if = "ruma_common::serde::is_default",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub users_default: Int,
#[serde(default, skip_serializing_if = "NotificationPowerLevels::is_default")]
pub notifications: NotificationPowerLevels,
}
impl RoomPowerLevelsEventContent {
pub fn new() -> Self {
Self {
ban: default_power_level(),
events: BTreeMap::new(),
events_default: int!(0),
invite: int!(0),
kick: default_power_level(),
redact: default_power_level(),
state_default: default_power_level(),
users: BTreeMap::new(),
users_default: int!(0),
notifications: NotificationPowerLevels::default(),
}
}
}
impl Default for RoomPowerLevelsEventContent {
fn default() -> Self {
Self::new()
}
}
impl RedactContent for RoomPowerLevelsEventContent {
type Redacted = RedactedRoomPowerLevelsEventContent;
fn redact(self, version: &RoomVersionId) -> Self::Redacted {
let Self {
ban,
events,
events_default,
invite,
kick,
redact,
state_default,
users,
users_default,
..
} = self;
let invite = match version {
RoomVersionId::V1
| RoomVersionId::V2
| RoomVersionId::V3
| RoomVersionId::V4
| RoomVersionId::V5
| RoomVersionId::V6
| RoomVersionId::V7
| RoomVersionId::V8
| RoomVersionId::V9
| RoomVersionId::V10 => int!(0),
_ => invite,
};
RedactedRoomPowerLevelsEventContent {
ban,
events,
events_default,
invite,
kick,
redact,
state_default,
users,
users_default,
}
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_default_power_level(l: &Int) -> bool {
*l == int!(50)
}
impl RoomPowerLevelsEvent {
pub fn power_levels(&self) -> RoomPowerLevels {
match self {
Self::Original(ev) => ev.content.clone().into(),
Self::Redacted(ev) => ev.content.clone().into(),
}
}
}
impl SyncRoomPowerLevelsEvent {
pub fn power_levels(&self) -> RoomPowerLevels {
match self {
Self::Original(ev) => ev.content.clone().into(),
Self::Redacted(ev) => ev.content.clone().into(),
}
}
}
impl StrippedRoomPowerLevelsEvent {
pub fn power_levels(&self) -> RoomPowerLevels {
self.content.clone().into()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct RedactedRoomPowerLevelsEventContent {
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub ban: Int,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "ruma_common::serde::btreemap_deserialize_v1_powerlevel_values"
)]
pub events: BTreeMap<TimelineEventType, Int>,
#[serde(
default,
skip_serializing_if = "ruma_common::serde::is_default",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub events_default: Int,
#[serde(
default,
skip_serializing_if = "ruma_common::serde::is_default",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub invite: Int,
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub kick: Int,
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub redact: Int,
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub state_default: Int,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "ruma_common::serde::btreemap_deserialize_v1_powerlevel_values"
)]
pub users: BTreeMap<OwnedUserId, Int>,
#[serde(
default,
skip_serializing_if = "ruma_common::serde::is_default",
deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel"
)]
pub users_default: Int,
}
impl EventContent for RedactedRoomPowerLevelsEventContent {
type EventType = StateEventType;
fn event_type(&self) -> Self::EventType {
StateEventType::RoomPowerLevels
}
}
impl StaticEventContent for RedactedRoomPowerLevelsEventContent {
const TYPE: &'static str = "m.room.power_levels";
}
impl RedactedStateEventContent for RedactedRoomPowerLevelsEventContent {
type StateKey = EmptyStateKey;
}
#[derive(Clone, Debug)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct RoomPowerLevels {
pub ban: Int,
pub events: BTreeMap<TimelineEventType, Int>,
pub events_default: Int,
pub invite: Int,
pub kick: Int,
pub redact: Int,
pub state_default: Int,
pub users: BTreeMap<OwnedUserId, Int>,
pub users_default: Int,
pub notifications: NotificationPowerLevels,
}
impl RoomPowerLevels {
pub fn for_user(&self, user_id: &UserId) -> Int {
self.users.get(user_id).map_or(self.users_default, |pl| *pl)
}
pub fn for_action(&self, action: PowerLevelAction) -> Int {
match action {
PowerLevelAction::Ban => self.ban,
PowerLevelAction::Unban => self.ban.max(self.kick),
PowerLevelAction::Invite => self.invite,
PowerLevelAction::Kick => self.kick,
PowerLevelAction::RedactOwn => self.for_message(MessageLikeEventType::RoomRedaction),
PowerLevelAction::RedactOther => {
self.redact.max(self.for_message(MessageLikeEventType::RoomRedaction))
}
PowerLevelAction::SendMessage(msg_type) => self.for_message(msg_type),
PowerLevelAction::SendState(state_type) => self.for_state(state_type),
PowerLevelAction::TriggerNotification(NotificationPowerLevelType::Room) => {
self.notifications.room
}
}
}
pub fn for_message(&self, msg_type: MessageLikeEventType) -> Int {
self.events.get(&msg_type.into()).copied().unwrap_or(self.events_default)
}
pub fn for_state(&self, state_type: StateEventType) -> Int {
self.events.get(&state_type.into()).copied().unwrap_or(self.state_default)
}
pub fn user_can_ban(&self, user_id: &UserId) -> bool {
self.for_user(user_id) >= self.ban
}
pub fn user_can_ban_user(&self, acting_user_id: &UserId, target_user_id: &UserId) -> bool {
let acting_pl = self.for_user(acting_user_id);
let target_pl = self.for_user(target_user_id);
acting_pl >= self.ban && target_pl < acting_pl
}
pub fn user_can_unban(&self, user_id: &UserId) -> bool {
let pl = self.for_user(user_id);
pl >= self.ban && pl >= self.kick
}
pub fn user_can_unban_user(&self, acting_user_id: &UserId, target_user_id: &UserId) -> bool {
let acting_pl = self.for_user(acting_user_id);
let target_pl = self.for_user(target_user_id);
acting_pl >= self.ban && acting_pl >= self.kick && target_pl < acting_pl
}
pub fn user_can_invite(&self, user_id: &UserId) -> bool {
self.for_user(user_id) >= self.invite
}
pub fn user_can_kick(&self, user_id: &UserId) -> bool {
self.for_user(user_id) >= self.kick
}
pub fn user_can_kick_user(&self, acting_user_id: &UserId, target_user_id: &UserId) -> bool {
let acting_pl = self.for_user(acting_user_id);
let target_pl = self.for_user(target_user_id);
acting_pl >= self.kick && target_pl < acting_pl
}
pub fn user_can_redact_own_event(&self, user_id: &UserId) -> bool {
self.user_can_send_message(user_id, MessageLikeEventType::RoomRedaction)
}
pub fn user_can_redact_event_of_other(&self, user_id: &UserId) -> bool {
self.user_can_redact_own_event(user_id) && self.for_user(user_id) >= self.redact
}
pub fn user_can_send_message(&self, user_id: &UserId, msg_type: MessageLikeEventType) -> bool {
self.for_user(user_id) >= self.for_message(msg_type)
}
pub fn user_can_send_state(&self, user_id: &UserId, state_type: StateEventType) -> bool {
self.for_user(user_id) >= self.for_state(state_type)
}
pub fn user_can_trigger_room_notification(&self, user_id: &UserId) -> bool {
self.for_user(user_id) >= self.notifications.room
}
pub fn user_can_change_user_power_level(
&self,
acting_user_id: &UserId,
target_user_id: &UserId,
) -> bool {
if !self.user_can_send_state(acting_user_id, StateEventType::RoomPowerLevels) {
return false;
}
if acting_user_id == target_user_id {
return true;
}
if let Some(target_pl) = self.users.get(target_user_id).copied() {
self.for_user(acting_user_id) > target_pl
} else {
true
}
}
pub fn user_can_do(&self, user_id: &UserId, action: PowerLevelAction) -> bool {
match action {
PowerLevelAction::Ban => self.user_can_ban(user_id),
PowerLevelAction::Unban => self.user_can_unban(user_id),
PowerLevelAction::Invite => self.user_can_invite(user_id),
PowerLevelAction::Kick => self.user_can_kick(user_id),
PowerLevelAction::RedactOwn => self.user_can_redact_own_event(user_id),
PowerLevelAction::RedactOther => self.user_can_redact_event_of_other(user_id),
PowerLevelAction::SendMessage(message_type) => {
self.user_can_send_message(user_id, message_type)
}
PowerLevelAction::SendState(state_type) => {
self.user_can_send_state(user_id, state_type)
}
PowerLevelAction::TriggerNotification(NotificationPowerLevelType::Room) => {
self.user_can_trigger_room_notification(user_id)
}
}
}
pub fn user_can_do_to_user(
&self,
acting_user_id: &UserId,
target_user_id: &UserId,
action: PowerLevelUserAction,
) -> bool {
match action {
PowerLevelUserAction::Ban => self.user_can_ban_user(acting_user_id, target_user_id),
PowerLevelUserAction::Unban => self.user_can_unban_user(acting_user_id, target_user_id),
PowerLevelUserAction::Invite => self.user_can_invite(acting_user_id),
PowerLevelUserAction::Kick => self.user_can_kick_user(acting_user_id, target_user_id),
PowerLevelUserAction::ChangePowerLevel => {
self.user_can_change_user_power_level(acting_user_id, target_user_id)
}
}
}
pub fn max(&self) -> Int {
self.users.values().fold(self.users_default, |max_pl, user_pl| max(max_pl, *user_pl))
}
}
impl From<RoomPowerLevelsEventContent> for RoomPowerLevels {
fn from(c: RoomPowerLevelsEventContent) -> Self {
Self {
ban: c.ban,
events: c.events,
events_default: c.events_default,
invite: c.invite,
kick: c.kick,
redact: c.redact,
state_default: c.state_default,
users: c.users,
users_default: c.users_default,
notifications: c.notifications,
}
}
}
impl From<RedactedRoomPowerLevelsEventContent> for RoomPowerLevels {
fn from(c: RedactedRoomPowerLevelsEventContent) -> Self {
Self {
ban: c.ban,
events: c.events,
events_default: c.events_default,
invite: c.invite,
kick: c.kick,
redact: c.redact,
state_default: c.state_default,
users: c.users,
users_default: c.users_default,
notifications: NotificationPowerLevels::default(),
}
}
}
impl From<RoomPowerLevels> for RoomPowerLevelsEventContent {
fn from(c: RoomPowerLevels) -> Self {
Self {
ban: c.ban,
events: c.events,
events_default: c.events_default,
invite: c.invite,
kick: c.kick,
redact: c.redact,
state_default: c.state_default,
users: c.users,
users_default: c.users_default,
notifications: c.notifications,
}
}
}
impl From<RoomPowerLevels> for PushConditionPowerLevelsCtx {
fn from(c: RoomPowerLevels) -> Self {
Self { users: c.users, users_default: c.users_default, notifications: c.notifications }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PowerLevelAction {
Ban,
Unban,
Invite,
Kick,
RedactOwn,
RedactOther,
SendMessage(MessageLikeEventType),
SendState(StateEventType),
TriggerNotification(NotificationPowerLevelType),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum NotificationPowerLevelType {
Room,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PowerLevelUserAction {
Ban,
Unban,
Invite,
Kick,
ChangePowerLevel,
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use assign::assign;
use js_int::int;
use maplit::btreemap;
use ruma_common::user_id;
use serde_json::{json, to_value as to_json_value};
use super::{default_power_level, NotificationPowerLevels, RoomPowerLevelsEventContent};
#[test]
fn serialization_with_optional_fields_as_none() {
let default = default_power_level();
let power_levels = RoomPowerLevelsEventContent {
ban: default,
events: BTreeMap::new(),
events_default: int!(0),
invite: int!(0),
kick: default,
redact: default,
state_default: default,
users: BTreeMap::new(),
users_default: int!(0),
notifications: NotificationPowerLevels::default(),
};
let actual = to_json_value(&power_levels).unwrap();
let expected = json!({});
assert_eq!(actual, expected);
}
#[test]
fn serialization_with_all_fields() {
let user = user_id!("@carl:example.com");
let power_levels_event = RoomPowerLevelsEventContent {
ban: int!(23),
events: btreemap! {
"m.dummy".into() => int!(23)
},
events_default: int!(23),
invite: int!(23),
kick: int!(23),
redact: int!(23),
state_default: int!(23),
users: btreemap! {
user.to_owned() => int!(23)
},
users_default: int!(23),
notifications: assign!(NotificationPowerLevels::new(), { room: int!(23) }),
};
let actual = to_json_value(&power_levels_event).unwrap();
let expected = json!({
"ban": 23,
"events": {
"m.dummy": 23
},
"events_default": 23,
"invite": 23,
"kick": 23,
"redact": 23,
"state_default": 23,
"users": {
"@carl:example.com": 23
},
"users_default": 23,
"notifications": {
"room": 23
},
});
assert_eq!(actual, expected);
}
}