fractal/session/model/room/
permissions.rsuse std::fmt;
use gettextrs::gettext;
use gtk::{
glib,
glib::{clone, closure_local},
prelude::*,
subclass::prelude::*,
};
use matrix_sdk::event_handler::EventHandlerDropGuard;
use ruma::{
events::{
room::power_levels::{
PowerLevelAction, PowerLevelUserAction, RoomPowerLevels, RoomPowerLevelsEventContent,
},
MessageLikeEventType, StateEventType, SyncStateEvent,
},
Int, OwnedUserId, UserId,
};
use tracing::error;
use super::{Member, Membership, Room};
use crate::{prelude::*, spawn, spawn_tokio};
pub type PowerLevel = i64;
pub const POWER_LEVEL_MAX: PowerLevel = 0x001F_FFFF_FFFF_FFFF;
pub const POWER_LEVEL_MIN: PowerLevel = -POWER_LEVEL_MAX;
pub const POWER_LEVEL_ADMIN: PowerLevel = 100;
pub const POWER_LEVEL_MOD: PowerLevel = 50;
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[enum_type(name = "MemberRole")]
pub enum MemberRole {
#[default]
Default,
Custom,
Moderator,
Administrator,
Muted,
}
impl fmt::Display for MemberRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Default => write!(f, "{}", gettext("Default")),
Self::Custom => write!(f, "{}", gettext("Custom")),
Self::Moderator => write!(f, "{}", gettext("Moderator")),
Self::Administrator => write!(f, "{}", gettext("Admin")),
Self::Muted => write!(f, "{}", gettext("Muted")),
}
}
}
mod imp {
use std::{
cell::{Cell, OnceCell, RefCell},
sync::LazyLock,
};
use glib::subclass::Signal;
use ruma::events::room::power_levels::NotificationPowerLevelType;
use super::*;
#[derive(Debug, glib::Properties)]
#[properties(wrapper_type = super::Permissions)]
pub struct Permissions {
#[property(get)]
pub(super) room: glib::WeakRef<Room>,
pub(super) power_levels: RefCell<RoomPowerLevels>,
power_levels_drop_guard: OnceCell<EventHandlerDropGuard>,
#[property(get)]
is_joined: Cell<bool>,
#[property(get)]
own_power_level: Cell<PowerLevel>,
#[property(get)]
default_power_level: Cell<PowerLevel>,
#[property(get)]
mute_power_level: Cell<PowerLevel>,
#[property(get)]
can_change_avatar: Cell<bool>,
#[property(get)]
can_change_name: Cell<bool>,
#[property(get)]
can_change_topic: Cell<bool>,
#[property(get)]
can_invite: Cell<bool>,
#[property(get)]
can_send_message: Cell<bool>,
#[property(get)]
can_send_reaction: Cell<bool>,
#[property(get)]
can_redact_own: Cell<bool>,
#[property(get)]
can_redact_other: Cell<bool>,
#[property(get)]
can_notify_room: Cell<bool>,
}
impl Default for Permissions {
fn default() -> Self {
Self {
room: Default::default(),
power_levels: RefCell::new(RoomPowerLevelsEventContent::default().into()),
power_levels_drop_guard: Default::default(),
is_joined: Default::default(),
own_power_level: Default::default(),
default_power_level: Default::default(),
mute_power_level: Default::default(),
can_change_avatar: Default::default(),
can_change_name: Default::default(),
can_change_topic: Default::default(),
can_invite: Default::default(),
can_send_message: Default::default(),
can_send_reaction: Default::default(),
can_redact_own: Default::default(),
can_redact_other: Default::default(),
can_notify_room: Default::default(),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for Permissions {
const NAME: &'static str = "RoomPermissions";
type Type = super::Permissions;
}
#[glib::derived_properties]
impl ObjectImpl for Permissions {
fn signals() -> &'static [Signal] {
static SIGNALS: LazyLock<Vec<Signal>> =
LazyLock::new(|| vec![Signal::builder("changed").build()]);
SIGNALS.as_ref()
}
}
impl Permissions {
pub(super) fn init_own_member(&self, own_member: &Member) {
own_member.connect_membership_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_is_joined();
}
));
self.update_is_joined();
}
pub(super) fn own_member(&self) -> Option<Member> {
self.room.upgrade().map(|r| r.own_member())
}
pub(super) async fn init_power_levels(&self) {
let Some(room) = self.room.upgrade() else {
return;
};
let matrix_room = room.matrix_room();
let matrix_room_clone = matrix_room.clone();
let handle = spawn_tokio!(async move { matrix_room_clone.power_levels().await });
match handle.await.expect("task was not aborted") {
Ok(power_levels) => self.update_power_levels(&power_levels),
Err(error) => {
error!("Could not load room power levels: {error}");
}
}
let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
let handle = matrix_room.add_event_handler(
move |event: SyncStateEvent<RoomPowerLevelsEventContent>| {
let obj_weak = obj_weak.clone();
async move {
let ctx = glib::MainContext::default();
ctx.spawn(async move {
spawn!(async move {
if let Some(obj) = obj_weak.upgrade() {
obj.imp().update_power_levels(&event.power_levels());
}
});
});
}
},
);
let drop_guard = matrix_room.client().event_handler_drop_guard(handle);
self.power_levels_drop_guard
.set(drop_guard)
.expect("power levels drop guard is uninitialized");
}
fn update_is_joined(&self) {
let Some(own_member) = self.own_member() else {
return;
};
let is_joined = own_member.membership() == Membership::Join;
if self.is_joined.get() == is_joined {
return;
}
self.is_joined.set(is_joined);
self.permissions_changed();
}
fn update_power_levels(&self, power_levels: &RoomPowerLevels) {
self.power_levels.replace(power_levels.clone());
self.permissions_changed();
if let Some(room) = self.room.upgrade() {
if let Some(members) = room.members() {
members.update_power_levels(power_levels);
} else {
let own_member = room.own_member();
let own_user_id = own_member.user_id();
own_member.set_power_level(power_levels.for_user(own_user_id).into());
}
}
}
fn permissions_changed(&self) {
self.update_own_power_level();
self.update_default_power_level();
self.update_mute_power_level();
self.update_can_change_avatar();
self.update_can_change_name();
self.update_can_change_topic();
self.update_can_invite();
self.update_can_send_message();
self.update_can_send_reaction();
self.update_can_redact_own();
self.update_can_redact_other();
self.update_can_notify_room();
self.obj().emit_by_name::<()>("changed", &[]);
}
fn update_own_power_level(&self) {
let Some(room) = self.room.upgrade() else {
return;
};
let own_member = room.own_member();
let power_level = self
.power_levels
.borrow()
.for_user(own_member.user_id())
.into();
if self.own_power_level.get() == power_level {
return;
}
self.own_power_level.set(power_level);
self.obj().notify_own_power_level();
}
fn update_default_power_level(&self) {
let power_level = self.power_levels.borrow().users_default.into();
if self.default_power_level.get() == power_level {
return;
}
self.default_power_level.set(power_level);
self.obj().notify_default_power_level();
}
fn update_mute_power_level(&self) {
let power_levels = self.power_levels.borrow();
let message_power_level = power_levels
.events
.get(&MessageLikeEventType::RoomMessage.into())
.copied()
.unwrap_or(power_levels.events_default);
let power_level = (-1).min(message_power_level.into());
if self.mute_power_level.get() == power_level {
return;
}
self.mute_power_level.set(power_level);
self.obj().notify_mute_power_level();
}
pub(super) fn is_allowed_to(&self, room_action: PowerLevelAction) -> bool {
if !self.is_joined.get() {
return false;
}
let Some(own_member) = self.own_member() else {
return false;
};
self.power_levels
.borrow()
.user_can_do(own_member.user_id(), room_action)
}
fn update_can_change_avatar(&self) {
let can_change_avatar =
self.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomAvatar));
if self.can_change_avatar.get() == can_change_avatar {
return;
};
self.can_change_avatar.set(can_change_avatar);
self.obj().notify_can_change_avatar();
}
fn update_can_change_name(&self) {
let can_change_name =
self.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomName));
if self.can_change_name.get() == can_change_name {
return;
};
self.can_change_name.set(can_change_name);
self.obj().notify_can_change_name();
}
fn update_can_change_topic(&self) {
let can_change_topic =
self.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomTopic));
if self.can_change_topic.get() == can_change_topic {
return;
};
self.can_change_topic.set(can_change_topic);
self.obj().notify_can_change_topic();
}
fn update_can_invite(&self) {
let can_invite = self.is_allowed_to(PowerLevelAction::Invite);
if self.can_invite.get() == can_invite {
return;
};
self.can_invite.set(can_invite);
self.obj().notify_can_invite();
}
fn update_can_send_message(&self) {
let can_send_message = self.is_allowed_to(PowerLevelAction::SendMessage(
MessageLikeEventType::RoomMessage,
));
if self.can_send_message.get() == can_send_message {
return;
};
self.can_send_message.set(can_send_message);
self.obj().notify_can_send_message();
}
fn update_can_send_reaction(&self) {
let can_send_reaction = self.is_allowed_to(PowerLevelAction::SendMessage(
MessageLikeEventType::Reaction,
));
if self.can_send_reaction.get() == can_send_reaction {
return;
};
self.can_send_reaction.set(can_send_reaction);
self.obj().notify_can_send_reaction();
}
fn update_can_redact_own(&self) {
let can_redact_own = self.is_allowed_to(PowerLevelAction::RedactOwn);
if self.can_redact_own.get() == can_redact_own {
return;
};
self.can_redact_own.set(can_redact_own);
self.obj().notify_can_redact_own();
}
fn update_can_redact_other(&self) {
let can_redact_other = self.is_allowed_to(PowerLevelAction::RedactOther);
if self.can_redact_other.get() == can_redact_other {
return;
};
self.can_redact_other.set(can_redact_other);
self.obj().notify_can_redact_other();
}
fn update_can_notify_room(&self) {
let can_notify_room = self.is_allowed_to(PowerLevelAction::TriggerNotification(
NotificationPowerLevelType::Room,
));
if self.can_notify_room.get() == can_notify_room {
return;
};
self.can_notify_room.set(can_notify_room);
self.obj().notify_can_notify_room();
}
}
}
glib::wrapper! {
pub struct Permissions(ObjectSubclass<imp::Permissions>);
}
impl Permissions {
pub fn new() -> Self {
glib::Object::new()
}
pub(super) async fn init(&self, room: &Room) {
let imp = self.imp();
imp.room.set(Some(room));
imp.init_own_member(&room.own_member());
imp.init_power_levels().await;
}
pub(crate) fn power_levels(&self) -> RoomPowerLevels {
self.imp().power_levels.borrow().clone()
}
pub(crate) fn role(&self, power_level: PowerLevel) -> MemberRole {
if power_level >= POWER_LEVEL_ADMIN {
MemberRole::Administrator
} else if power_level >= POWER_LEVEL_MOD {
MemberRole::Moderator
} else if power_level == self.default_power_level() {
MemberRole::Default
} else if power_level < self.default_power_level() && power_level <= self.mute_power_level()
{
MemberRole::Muted
} else {
MemberRole::Custom
}
}
pub(crate) fn is_allowed_to(&self, room_action: PowerLevelAction) -> bool {
self.imp().is_allowed_to(room_action)
}
pub(crate) fn can_do_to_user(&self, user_id: &UserId, action: PowerLevelUserAction) -> bool {
let imp = self.imp();
if !self.is_joined() {
return false;
}
let Some(own_member) = imp.own_member() else {
return false;
};
let own_user_id = own_member.user_id();
let power_levels = imp.power_levels.borrow();
if own_user_id == user_id {
return action == PowerLevelUserAction::ChangePowerLevel
&& power_levels.user_can_send_state(own_user_id, StateEventType::RoomPowerLevels);
}
power_levels.user_can_do_to_user(own_user_id, user_id, action)
}
pub(crate) async fn set_user_power_level(
&self,
user_id: OwnedUserId,
power_level: PowerLevel,
) -> Result<(), ()> {
let Some(room) = self.room() else {
return Err(());
};
let matrix_room = room.matrix_room().clone();
let handle = spawn_tokio!(async move {
let power_level = Int::new_saturating(power_level);
matrix_room
.update_power_levels(vec![(&user_id, power_level)])
.await
});
match handle.await.expect("task was not aborted") {
Ok(_) => Ok(()),
Err(error) => {
error!("Could not set user power level: {error}");
Err(())
}
}
}
pub(crate) async fn set_power_levels(&self, power_levels: RoomPowerLevels) -> Result<(), ()> {
let Some(room) = self.room() else {
return Err(());
};
let matrix_room = room.matrix_room().clone();
let handle = spawn_tokio!(async move {
let event = RoomPowerLevelsEventContent::from(power_levels);
matrix_room.send_state_event(event).await
});
match handle.await.expect("task was not aborted") {
Ok(_) => Ok(()),
Err(error) => {
error!("Failed to set power levels: {error}");
Err(())
}
}
}
pub(crate) fn user_is_allowed_to(
&self,
user_id: &UserId,
room_action: PowerLevelAction,
) -> bool {
self.imp()
.power_levels
.borrow()
.user_can_do(user_id, room_action)
}
pub(crate) fn connect_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(
"changed",
true,
closure_local!(move |obj: Self| {
f(&obj);
}),
)
}
}
impl Default for Permissions {
fn default() -> Self {
Self::new()
}
}