Compare commits
4 commits
11c04d1458
...
1f91a74b27
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f91a74b27 | |||
| 5f901a560b | |||
| 59401e1786 | |||
| 95fa3b022a |
2 changed files with 412 additions and 349 deletions
|
|
@ -81,6 +81,54 @@ macro_rules! custom_state_pdu {
|
|||
};
|
||||
}
|
||||
|
||||
/// Cascade-remove a role name from all state events of a given type. For each
|
||||
/// event that contains the role, the `$field` is filtered and the updated
|
||||
/// content is sent back as a new state event.
|
||||
macro_rules! cascade_remove_role {
|
||||
(
|
||||
$self:expr,
|
||||
$shortstatehash:expr,
|
||||
$event_type_fn:expr,
|
||||
$event_type_const:expr,
|
||||
$content_ty:ty,
|
||||
$field:ident,
|
||||
$role_name:expr,
|
||||
$space_id:expr,
|
||||
$state_lock:expr,
|
||||
$server_user:expr
|
||||
) => {{
|
||||
let ev_type = $event_type_fn;
|
||||
let entries: Vec<(_, ruma::OwnedEventId)> = $self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_keys_with_ids($shortstatehash, &ev_type)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for (state_key, event_id) in entries {
|
||||
if let Ok(pdu) = $self.services.rooms.timeline.get_pdu(&event_id).await {
|
||||
if let Ok(mut content) = pdu.get_content::<$content_ty>() {
|
||||
if content.$field.contains($role_name) {
|
||||
content.$field.retain(|r| r != $role_name);
|
||||
$self
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!($event_type_const, &state_key, &content),
|
||||
$server_user,
|
||||
Some(&$space_id),
|
||||
&$state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! send_space_state {
|
||||
($self:expr, $space_id:expr, $event_type:expr, $state_key:expr, $content:expr) => {{
|
||||
let state_lock = $self.services.rooms.state.mutex.lock(&$space_id).await;
|
||||
|
|
@ -265,7 +313,7 @@ async fn remove(&self, space: OwnedRoomOrAliasId, role_name: String) -> Result {
|
|||
|
||||
send_space_state!(self, space_id, SPACE_ROLES_EVENT_TYPE, "", &content);
|
||||
|
||||
let member_event_type = member_event_type();
|
||||
// Cascade: remove the deleted role from all member and room events
|
||||
let server_user = &self.services.globals.server_user;
|
||||
if let Ok(shortstatehash) = self
|
||||
.services
|
||||
|
|
@ -275,72 +323,32 @@ async fn remove(&self, space: OwnedRoomOrAliasId, role_name: String) -> Result {
|
|||
.await
|
||||
{
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let user_entries: Vec<(_, ruma::OwnedEventId)> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_keys_with_ids(shortstatehash, &member_event_type)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for (state_key, event_id) in user_entries {
|
||||
if let Ok(pdu) = self.services.rooms.timeline.get_pdu(&event_id).await {
|
||||
if let Ok(mut member_content) = pdu.get_content::<SpaceRoleMemberEventContent>() {
|
||||
if member_content.roles.contains(&role_name) {
|
||||
member_content.roles.retain(|r| r != &role_name);
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(
|
||||
SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
&state_key,
|
||||
&member_content
|
||||
),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cascade_remove_role!(
|
||||
self,
|
||||
shortstatehash,
|
||||
member_event_type(),
|
||||
SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
SpaceRoleMemberEventContent,
|
||||
roles,
|
||||
&role_name,
|
||||
space_id,
|
||||
state_lock,
|
||||
server_user
|
||||
);
|
||||
|
||||
// Cascade: remove the role from all rooms' role requirement events
|
||||
let room_event_type = room_event_type();
|
||||
|
||||
let room_entries: Vec<(_, ruma::OwnedEventId)> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_keys_with_ids(shortstatehash, &room_event_type)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for (state_key, event_id) in room_entries {
|
||||
if let Ok(pdu) = self.services.rooms.timeline.get_pdu(&event_id).await {
|
||||
if let Ok(mut room_content) = pdu.get_content::<SpaceRoleRoomEventContent>() {
|
||||
if room_content.required_roles.contains(&role_name) {
|
||||
room_content.required_roles.retain(|r| r != &role_name);
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(
|
||||
SPACE_ROLE_ROOM_EVENT_TYPE,
|
||||
&state_key,
|
||||
&room_content
|
||||
),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cascade_remove_role!(
|
||||
self,
|
||||
shortstatehash,
|
||||
room_event_type(),
|
||||
SPACE_ROLE_ROOM_EVENT_TYPE,
|
||||
SpaceRoleRoomEventContent,
|
||||
required_roles,
|
||||
&role_name,
|
||||
space_id,
|
||||
state_lock,
|
||||
server_user
|
||||
);
|
||||
}
|
||||
|
||||
self.write_str(&format!("Removed role '{role_name}' from space {space_id}."))
|
||||
|
|
|
|||
|
|
@ -13,18 +13,12 @@ use conduwuit::{
|
|||
matrix::pdu::{PduBuilder, PduEvent},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_core::{
|
||||
matrix::space_roles::{
|
||||
RoleDefinition, SPACE_CASCADING_EVENT_TYPE, SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
SPACE_ROLE_ROOM_EVENT_TYPE, SPACE_ROLES_EVENT_TYPE, SpaceCascadingEventContent,
|
||||
SpaceRoleMemberEventContent, SpaceRoleRoomEventContent, SpaceRolesEventContent,
|
||||
},
|
||||
utils::{
|
||||
future::TryExtExt,
|
||||
stream::{BroadbandExt, ReadyExt},
|
||||
},
|
||||
use conduwuit_core::matrix::space_roles::{
|
||||
RoleDefinition, SPACE_CASCADING_EVENT_TYPE, SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
SPACE_ROLE_ROOM_EVENT_TYPE, SPACE_ROLES_EVENT_TYPE, SpaceCascadingEventContent,
|
||||
SpaceRoleMemberEventContent, SpaceRoleRoomEventContent, SpaceRolesEventContent,
|
||||
};
|
||||
use futures::{StreamExt, TryFutureExt};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
Int, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
events::{
|
||||
|
|
@ -252,6 +246,118 @@ pub async fn ensure_default_roles(&self, space_id: &RoomId) -> Result {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns all `(state_key, pdu)` pairs for state events of the given type
|
||||
/// within the room state identified by `shortstatehash`.
|
||||
#[implement(Service)]
|
||||
async fn load_state_pdus(
|
||||
&self,
|
||||
shortstatehash: u64,
|
||||
event_type: &StateEventType,
|
||||
) -> Vec<(conduwuit_core::matrix::StateKey, PduEvent)> {
|
||||
let entries: Vec<(_, OwnedEventId)> = self
|
||||
.services
|
||||
.state_accessor
|
||||
.state_keys_with_ids(shortstatehash, event_type)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let mut result = Vec::with_capacity(entries.len());
|
||||
for (state_key, event_id) in entries {
|
||||
if let Ok(pdu) = self.services.timeline.get_pdu(&event_id).await {
|
||||
result.push((state_key, pdu));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Loads all `SpaceRoleMember` state events and writes the resulting
|
||||
/// user-to-roles mapping into the `user_roles` cache.
|
||||
#[implement(Service)]
|
||||
async fn load_user_roles(&self, space_id: &RoomId, shortstatehash: u64) {
|
||||
let member_event_type = member_event_type();
|
||||
let mut user_roles_map: HashMap<OwnedUserId, HashSet<String>> = HashMap::new();
|
||||
|
||||
for (state_key, pdu) in self
|
||||
.load_state_pdus(shortstatehash, &member_event_type)
|
||||
.await
|
||||
{
|
||||
if let Ok(content) = pdu.get_content::<SpaceRoleMemberEventContent>() {
|
||||
if let Ok(user_id) = UserId::parse(&*state_key) {
|
||||
user_roles_map.insert(user_id.to_owned(), content.roles.into_iter().collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.user_roles
|
||||
.write()
|
||||
.await
|
||||
.insert(space_id.to_owned(), user_roles_map);
|
||||
}
|
||||
|
||||
/// Loads all `SpaceRoleRoom` state events and writes the resulting
|
||||
/// room-to-required-roles mapping into the `room_requirements` cache.
|
||||
#[implement(Service)]
|
||||
async fn load_room_requirements(&self, space_id: &RoomId, shortstatehash: u64) {
|
||||
let room_event_type = room_event_type();
|
||||
let mut room_reqs_map: HashMap<OwnedRoomId, HashSet<String>> = HashMap::new();
|
||||
|
||||
for (state_key, pdu) in self.load_state_pdus(shortstatehash, &room_event_type).await {
|
||||
if let Ok(content) = pdu.get_content::<SpaceRoleRoomEventContent>() {
|
||||
if let Ok(room_id) = RoomId::parse(&*state_key) {
|
||||
room_reqs_map
|
||||
.insert(room_id.to_owned(), content.required_roles.into_iter().collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.room_requirements
|
||||
.write()
|
||||
.await
|
||||
.insert(space_id.to_owned(), room_reqs_map);
|
||||
}
|
||||
|
||||
/// Loads all `SpaceChild` state events and updates both the
|
||||
/// `room_to_space` and `space_to_rooms` indexes.
|
||||
#[implement(Service)]
|
||||
async fn load_child_rooms_index(&self, space_id: &RoomId, shortstatehash: u64) {
|
||||
let mut child_rooms: Vec<OwnedRoomId> = Vec::new();
|
||||
|
||||
for (state_key, pdu) in self
|
||||
.load_state_pdus(shortstatehash, &StateEventType::SpaceChild)
|
||||
.await
|
||||
{
|
||||
if let Ok(content) = pdu.get_content::<SpaceChildEventContent>() {
|
||||
if content.via.is_empty() {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if let Ok(child_room_id) = RoomId::parse(&*state_key) {
|
||||
child_rooms.push(child_room_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut room_to_space = self.room_to_space.write().await;
|
||||
room_to_space.retain(|_, parents| {
|
||||
parents.remove(space_id);
|
||||
!parents.is_empty()
|
||||
});
|
||||
for child_room_id in &child_rooms {
|
||||
room_to_space
|
||||
.entry(child_room_id.clone())
|
||||
.or_default()
|
||||
.insert(space_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut space_to_rooms = self.space_to_rooms.write().await;
|
||||
space_to_rooms.insert(space_id.to_owned(), child_rooms.into_iter().collect())
|
||||
};
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
pub async fn populate_space(&self, space_id: &RoomId) {
|
||||
if !self.is_enabled_for_space(space_id).await {
|
||||
|
|
@ -279,7 +385,6 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
.insert(space_id.to_owned(), content.roles);
|
||||
}
|
||||
|
||||
let member_event_type = member_event_type();
|
||||
let shortstatehash = match self.services.state.get_room_shortstatehash(space_id).await {
|
||||
| Ok(hash) => hash,
|
||||
| Err(e) => {
|
||||
|
|
@ -287,118 +392,10 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
return;
|
||||
},
|
||||
};
|
||||
{
|
||||
let mut user_roles_map: HashMap<OwnedUserId, HashSet<String>> = HashMap::new();
|
||||
|
||||
self.services
|
||||
.state_accessor
|
||||
.state_keys_with_ids(shortstatehash, &member_event_type)
|
||||
.boxed()
|
||||
.broad_filter_map(|(state_key, event_id): (_, OwnedEventId)| async move {
|
||||
self.services
|
||||
.timeline
|
||||
.get_pdu(&event_id)
|
||||
.map_ok(move |pdu| (state_key, pdu))
|
||||
.ok()
|
||||
.await
|
||||
})
|
||||
.ready_filter_map(|(state_key, pdu)| {
|
||||
let content = pdu.get_content::<SpaceRoleMemberEventContent>().ok()?;
|
||||
let user_id = UserId::parse(&*state_key).ok()?.to_owned();
|
||||
Some((user_id, content.roles))
|
||||
})
|
||||
.for_each(|(user_id, roles)| {
|
||||
user_roles_map.insert(user_id, roles.into_iter().collect());
|
||||
async {}
|
||||
})
|
||||
.await;
|
||||
|
||||
self.user_roles
|
||||
.write()
|
||||
.await
|
||||
.insert(space_id.to_owned(), user_roles_map);
|
||||
|
||||
let room_event_type = room_event_type();
|
||||
let mut room_reqs_map: HashMap<OwnedRoomId, HashSet<String>> = HashMap::new();
|
||||
|
||||
self.services
|
||||
.state_accessor
|
||||
.state_keys_with_ids(shortstatehash, &room_event_type)
|
||||
.boxed()
|
||||
.broad_filter_map(|(state_key, event_id): (_, OwnedEventId)| async move {
|
||||
self.services
|
||||
.timeline
|
||||
.get_pdu(&event_id)
|
||||
.map_ok(move |pdu| (state_key, pdu))
|
||||
.ok()
|
||||
.await
|
||||
})
|
||||
.ready_filter_map(|(state_key, pdu)| {
|
||||
let content = pdu.get_content::<SpaceRoleRoomEventContent>().ok()?;
|
||||
let room_id = RoomId::parse(&*state_key).ok()?.to_owned();
|
||||
Some((room_id, content.required_roles))
|
||||
})
|
||||
.for_each(|(room_id, required_roles)| {
|
||||
room_reqs_map.insert(room_id, required_roles.into_iter().collect());
|
||||
async {}
|
||||
})
|
||||
.await;
|
||||
|
||||
self.room_requirements
|
||||
.write()
|
||||
.await
|
||||
.insert(space_id.to_owned(), room_reqs_map);
|
||||
|
||||
let mut child_rooms: Vec<OwnedRoomId> = Vec::new();
|
||||
|
||||
self.services
|
||||
.state_accessor
|
||||
.state_keys_with_ids(shortstatehash, &StateEventType::SpaceChild)
|
||||
.boxed()
|
||||
.broad_filter_map(|(state_key, event_id): (_, OwnedEventId)| async move {
|
||||
self.services
|
||||
.timeline
|
||||
.get_pdu(&event_id)
|
||||
.map_ok(move |pdu| (state_key, pdu))
|
||||
.ok()
|
||||
.await
|
||||
})
|
||||
.ready_filter_map(|(state_key, pdu)| {
|
||||
if let Ok(content) = pdu.get_content::<SpaceChildEventContent>() {
|
||||
if content.via.is_empty() {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
let child_room_id = RoomId::parse(&*state_key).ok()?.to_owned();
|
||||
Some(child_room_id)
|
||||
})
|
||||
.for_each(|child_room_id| {
|
||||
child_rooms.push(child_room_id);
|
||||
async {}
|
||||
})
|
||||
.await;
|
||||
|
||||
{
|
||||
let mut room_to_space = self.room_to_space.write().await;
|
||||
room_to_space.retain(|_, parents| {
|
||||
parents.remove(space_id);
|
||||
!parents.is_empty()
|
||||
});
|
||||
for child_room_id in &child_rooms {
|
||||
room_to_space
|
||||
.entry(child_room_id.clone())
|
||||
.or_default()
|
||||
.insert(space_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut space_to_rooms = self.space_to_rooms.write().await;
|
||||
space_to_rooms.insert(space_id.to_owned(), child_rooms.into_iter().collect())
|
||||
};
|
||||
}
|
||||
self.load_user_roles(space_id, shortstatehash).await;
|
||||
self.load_room_requirements(space_id, shortstatehash).await;
|
||||
self.load_child_rooms_index(space_id, shortstatehash).await;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -560,14 +557,7 @@ pub async fn sync_power_levels(&self, room_id: &RoomId) -> Result {
|
|||
continue;
|
||||
}
|
||||
|
||||
let mut max_pl: Option<i64> = None;
|
||||
for parent in &all_parents {
|
||||
if let Some(pl) = self.get_user_power_level(parent, user_id).await {
|
||||
max_pl = Some(max_pl.map_or(pl, |current| current.max(pl)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(effective_pl) = max_pl {
|
||||
if let Some(effective_pl) = self.compute_effective_pl(&all_parents, user_id).await {
|
||||
let effective_pl_int = Int::new_saturating(effective_pl);
|
||||
let current_pl = power_levels_content
|
||||
.users
|
||||
|
|
@ -601,6 +591,24 @@ pub async fn sync_power_levels(&self, room_id: &RoomId) -> Result {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Computes the maximum effective power level for a user across all given
|
||||
/// parent spaces. Returns `None` if the user has no power level defined in
|
||||
/// any parent.
|
||||
#[implement(Service)]
|
||||
async fn compute_effective_pl(
|
||||
&self,
|
||||
parent_spaces: &[OwnedRoomId],
|
||||
user_id: &UserId,
|
||||
) -> Option<i64> {
|
||||
let mut max_pl: Option<i64> = None;
|
||||
for parent in parent_spaces {
|
||||
if let Some(pl) = self.get_user_power_level(parent, user_id).await {
|
||||
max_pl = Some(max_pl.map_or(pl, |current| current.max(pl)));
|
||||
}
|
||||
}
|
||||
max_pl
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
pub async fn auto_join_qualifying_rooms(&self, space_id: &RoomId, user_id: &UserId) -> Result {
|
||||
if !self.is_enabled_for_space(space_id).await {
|
||||
|
|
@ -852,6 +860,66 @@ async fn sync_power_levels_for_children(&self, space_id: &RoomId) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enforces a change to the space role definitions: syncs power levels to
|
||||
/// all child rooms and kicks members who no longer qualify.
|
||||
#[implement(Service)]
|
||||
async fn enforce_roles_change(&self, space_id: &RoomId) {
|
||||
self.sync_power_levels_for_children(space_id).await;
|
||||
let space_members: Vec<OwnedUserId> = self
|
||||
.services
|
||||
.state_cache
|
||||
.room_members(space_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
for member in &space_members {
|
||||
if let Err(e) = Box::pin(self.kick_unqualified_from_rooms(space_id, member)).await {
|
||||
debug_warn!(user_id = %member, error = ?e, "Role definition revalidation kick failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforces a change to a user's role membership: auto-joins qualifying
|
||||
/// rooms, kicks from rooms they no longer qualify for, and syncs power
|
||||
/// levels.
|
||||
#[implement(Service)]
|
||||
async fn enforce_member_change(&self, space_id: &RoomId, user_id: &UserId) {
|
||||
if let Err(e) = self.auto_join_qualifying_rooms(space_id, user_id).await {
|
||||
debug_warn!(user_id = %user_id, error = ?e, "Space role auto-join failed");
|
||||
}
|
||||
if let Err(e) = Box::pin(self.kick_unqualified_from_rooms(space_id, user_id)).await {
|
||||
debug_warn!(user_id = %user_id, error = ?e, "Space role auto-kick failed");
|
||||
}
|
||||
self.sync_power_levels_for_children(space_id).await;
|
||||
}
|
||||
|
||||
/// Enforces a change to a room's role requirements: kicks all members of
|
||||
/// the target room who no longer meet the updated requirements.
|
||||
#[implement(Service)]
|
||||
async fn enforce_room_change(&self, space_id: &RoomId, target_room: &RoomId) {
|
||||
let members: Vec<OwnedUserId> = self
|
||||
.services
|
||||
.state_cache
|
||||
.room_members(target_room)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
for member in &members {
|
||||
if let Err(e) = Box::pin(self.kick_unqualified_from_rooms(space_id, member)).await {
|
||||
debug_warn!(user_id = %member, error = ?e, "Space role requirement kick failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforces a toggle of the cascading feature: if cascading was disabled,
|
||||
/// flushes the space from the cache.
|
||||
#[implement(Service)]
|
||||
async fn enforce_cascading_toggle(&self, space_id: &RoomId) {
|
||||
if !self.is_enabled_for_space(space_id).await {
|
||||
self.flush_space_from_cache(space_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn handle_state_event_change(
|
||||
self: &Arc<Self>,
|
||||
|
|
@ -886,62 +954,20 @@ impl Service {
|
|||
|
||||
match event_type.as_str() {
|
||||
| SPACE_ROLES_EVENT_TYPE => {
|
||||
this.sync_power_levels_for_children(&space_id).await;
|
||||
let space_members: Vec<OwnedUserId> = this
|
||||
.services
|
||||
.state_cache
|
||||
.room_members(&space_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
for member in &space_members {
|
||||
if let Err(e) =
|
||||
Box::pin(this.kick_unqualified_from_rooms(&space_id, member))
|
||||
.await
|
||||
{
|
||||
debug_warn!(user_id = %member, error = ?e, "Role definition revalidation kick failed");
|
||||
}
|
||||
}
|
||||
this.enforce_roles_change(&space_id).await;
|
||||
},
|
||||
| SPACE_ROLE_MEMBER_EVENT_TYPE => {
|
||||
if let Ok(user_id) = UserId::parse(state_key.as_str()) {
|
||||
if let Err(e) =
|
||||
this.auto_join_qualifying_rooms(&space_id, user_id).await
|
||||
{
|
||||
debug_warn!(user_id = %user_id, error = ?e, "Space role auto-join failed");
|
||||
}
|
||||
if let Err(e) =
|
||||
Box::pin(this.kick_unqualified_from_rooms(&space_id, user_id))
|
||||
.await
|
||||
{
|
||||
debug_warn!(user_id = %user_id, error = ?e, "Space role auto-kick failed");
|
||||
}
|
||||
this.sync_power_levels_for_children(&space_id).await;
|
||||
this.enforce_member_change(&space_id, user_id).await;
|
||||
}
|
||||
},
|
||||
| SPACE_ROLE_ROOM_EVENT_TYPE => {
|
||||
if let Ok(target_room) = RoomId::parse(state_key.as_str()) {
|
||||
let members: Vec<OwnedUserId> = this
|
||||
.services
|
||||
.state_cache
|
||||
.room_members(target_room)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
for member in &members {
|
||||
if let Err(e) =
|
||||
Box::pin(this.kick_unqualified_from_rooms(&space_id, member))
|
||||
.await
|
||||
{
|
||||
debug_warn!(user_id = %member, error = ?e, "Space role requirement kick failed");
|
||||
}
|
||||
}
|
||||
this.enforce_room_change(&space_id, target_room).await;
|
||||
}
|
||||
},
|
||||
| SPACE_CASCADING_EVENT_TYPE => {
|
||||
if !this.is_enabled_for_space(&space_id).await {
|
||||
this.flush_space_from_cache(&space_id).await;
|
||||
}
|
||||
this.enforce_cascading_toggle(&space_id).await;
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
|
|
@ -985,69 +1011,9 @@ impl Service {
|
|||
};
|
||||
|
||||
if is_removal {
|
||||
let mut room_to_space = this.room_to_space.write().await;
|
||||
if let Some(parents) = room_to_space.get_mut(&child_room_id) {
|
||||
parents.remove(&space_id);
|
||||
if parents.is_empty() {
|
||||
room_to_space.remove(&child_room_id);
|
||||
}
|
||||
}
|
||||
let mut space_to_rooms = this.space_to_rooms.write().await;
|
||||
if let Some(children) = space_to_rooms.get_mut(&space_id) {
|
||||
children.remove(&child_room_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.room_to_space
|
||||
.write()
|
||||
.await
|
||||
.entry(child_room_id.clone())
|
||||
.or_default()
|
||||
.insert(space_id.clone());
|
||||
|
||||
this.space_to_rooms
|
||||
.write()
|
||||
.await
|
||||
.entry(space_id.clone())
|
||||
.or_default()
|
||||
.insert(child_room_id.clone());
|
||||
|
||||
let server_user = this.services.globals.server_user.as_ref();
|
||||
if !this
|
||||
.services
|
||||
.state_cache
|
||||
.is_joined(server_user, &child_room_id)
|
||||
.await
|
||||
{
|
||||
debug_warn!(room_id = %child_room_id, "Server user is not joined, skipping auto-join enforcement for new child");
|
||||
return;
|
||||
}
|
||||
|
||||
let space_members: Vec<OwnedUserId> = this
|
||||
.services
|
||||
.state_cache
|
||||
.room_members(&space_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for member in &space_members {
|
||||
if this
|
||||
.user_qualifies_for_room(&space_id, &child_room_id, member)
|
||||
.await && !this
|
||||
.services
|
||||
.state_cache
|
||||
.is_joined(member, &child_room_id)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = this
|
||||
.invite_and_join_user(&child_room_id, member, server_user)
|
||||
.await
|
||||
{
|
||||
debug_warn!(user_id = %member, room_id = %child_room_id, error = ?e, "Failed to auto-join user");
|
||||
}
|
||||
}
|
||||
this.handle_child_removed(&space_id, &child_room_id).await;
|
||||
} else {
|
||||
this.handle_child_added(&space_id, &child_room_id).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1111,6 +1077,75 @@ impl Service {
|
|||
}
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
async fn handle_child_removed(&self, space_id: &RoomId, child_room_id: &RoomId) {
|
||||
let mut room_to_space = self.room_to_space.write().await;
|
||||
if let Some(parents) = room_to_space.get_mut(child_room_id) {
|
||||
parents.remove(space_id);
|
||||
if parents.is_empty() {
|
||||
room_to_space.remove(child_room_id);
|
||||
}
|
||||
}
|
||||
let mut space_to_rooms = self.space_to_rooms.write().await;
|
||||
if let Some(children) = space_to_rooms.get_mut(space_id) {
|
||||
children.remove(child_room_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
async fn handle_child_added(&self, space_id: &RoomId, child_room_id: &RoomId) {
|
||||
self.room_to_space
|
||||
.write()
|
||||
.await
|
||||
.entry(child_room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(space_id.to_owned());
|
||||
|
||||
self.space_to_rooms
|
||||
.write()
|
||||
.await
|
||||
.entry(space_id.to_owned())
|
||||
.or_default()
|
||||
.insert(child_room_id.to_owned());
|
||||
|
||||
let server_user = self.services.globals.server_user.as_ref();
|
||||
if !self
|
||||
.services
|
||||
.state_cache
|
||||
.is_joined(server_user, child_room_id)
|
||||
.await
|
||||
{
|
||||
debug_warn!(room_id = %child_room_id, "Server user is not joined, skipping auto-join enforcement for new child");
|
||||
return;
|
||||
}
|
||||
|
||||
let space_members: Vec<OwnedUserId> = self
|
||||
.services
|
||||
.state_cache
|
||||
.room_members(space_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for member in &space_members {
|
||||
if self
|
||||
.user_qualifies_for_room(space_id, child_room_id, member)
|
||||
.await && !self
|
||||
.services
|
||||
.state_cache
|
||||
.is_joined(member, child_room_id)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = self
|
||||
.invite_and_join_user(child_room_id, member, server_user)
|
||||
.await
|
||||
{
|
||||
debug_warn!(user_id = %member, room_id = %child_room_id, error = ?e, "Failed to auto-join user");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
pub async fn kick_unqualified_from_rooms(&self, space_id: &RoomId, user_id: &UserId) -> Result {
|
||||
if !self.is_enabled_for_space(space_id).await {
|
||||
|
|
@ -1149,53 +1184,73 @@ pub async fn kick_unqualified_from_rooms(&self, space_id: &RoomId, user_id: &Use
|
|||
continue;
|
||||
}
|
||||
|
||||
let all_parents = self.get_parent_spaces(child_room_id).await;
|
||||
let mut qualifies_in_any = false;
|
||||
for parent in &all_parents {
|
||||
if self
|
||||
.user_qualifies_for_room(parent, child_room_id, user_id)
|
||||
.await
|
||||
{
|
||||
qualifies_in_any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if qualifies_in_any {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(member_content) = self
|
||||
.services
|
||||
.state_accessor
|
||||
.get_member(child_room_id, user_id)
|
||||
.await
|
||||
else {
|
||||
debug_warn!(user_id = %user_id, room_id = %child_room_id, "Could not get member event, skipping kick");
|
||||
continue;
|
||||
};
|
||||
|
||||
let state_lock = self.services.state.mutex.lock(child_room_id).await;
|
||||
|
||||
if let Err(e) = self
|
||||
.services
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Leave,
|
||||
reason: Some("No longer has required Space roles".into()),
|
||||
is_direct: None,
|
||||
join_authorized_via_users_server: None,
|
||||
third_party_invite: None,
|
||||
..member_content
|
||||
}),
|
||||
server_user,
|
||||
Some(child_room_id),
|
||||
&state_lock,
|
||||
)
|
||||
if self
|
||||
.user_qualifies_in_any_parent(child_room_id, user_id)
|
||||
.await
|
||||
{
|
||||
warn!(user_id = %user_id, room_id = %child_room_id, error = ?e, "Failed to kick user for missing roles");
|
||||
continue;
|
||||
}
|
||||
|
||||
self.kick_user_from_room(child_room_id, user_id, server_user)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks whether the user qualifies for the given room through any of its
|
||||
/// parent spaces.
|
||||
#[implement(Service)]
|
||||
async fn user_qualifies_in_any_parent(&self, room_id: &RoomId, user_id: &UserId) -> bool {
|
||||
let all_parents = self.get_parent_spaces(room_id).await;
|
||||
for parent in &all_parents {
|
||||
if self.user_qualifies_for_room(parent, room_id, user_id).await {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Sends a kick (membership=leave) PDU to remove a user from a room for
|
||||
/// missing required Space roles.
|
||||
#[implement(Service)]
|
||||
async fn kick_user_from_room(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
server_user: &UserId,
|
||||
) -> Result {
|
||||
let Ok(member_content) = self
|
||||
.services
|
||||
.state_accessor
|
||||
.get_member(room_id, user_id)
|
||||
.await
|
||||
else {
|
||||
debug_warn!(user_id = %user_id, room_id = %room_id, "Could not get member event, skipping kick");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let state_lock = self.services.state.mutex.lock(room_id).await;
|
||||
|
||||
if let Err(e) = self
|
||||
.services
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Leave,
|
||||
reason: Some("No longer has required Space roles".into()),
|
||||
is_direct: None,
|
||||
join_authorized_via_users_server: None,
|
||||
third_party_invite: None,
|
||||
..member_content
|
||||
}),
|
||||
server_user,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(user_id = %user_id, room_id = %room_id, error = ?e, "Failed to kick user for missing roles");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue