refactor(spaces): consolidate duplicated code, delete redundant tests
Some checks failed
Documentation / Build and Deploy Documentation (pull_request) Has been skipped
Checks / Prek / Pre-commit & Formatting (pull_request) Failing after 5s
Checks / Prek / Clippy and Cargo Tests (pull_request) Failing after 5s
Update flake hashes / update-flake-hashes (pull_request) Failing after 5s
Some checks failed
Documentation / Build and Deploy Documentation (pull_request) Has been skipped
Checks / Prek / Pre-commit & Formatting (pull_request) Failing after 5s
Checks / Prek / Clippy and Cargo Tests (pull_request) Failing after 5s
Update flake hashes / update-flake-hashes (pull_request) Failing after 5s
- Delete cache_tests.rs and integration_tests.rs (only tested same two free functions already covered in tests.rs via MockCache indirection) - Extract invite_and_join_user helper, eliminating duplicate invite+join PDU pattern in auto_join and handle_space_child_change - Extract send_space_state! macro, eliminating 8 repeated lock+send blocks in admin commands - Extract resolve_room_as_space! macro for enable/disable/status - Remove redundant user_qualifies check in SPACE_ROLE_ROOM handler (kick_unqualified_from_rooms already checks) - Add parent_spaces.is_empty() short-circuit in build.rs PL enforcement - Hoist Arc::clone in append.rs to single binding - Rename space_roles_cache_capacity -> space_roles_cache_flush_threshold - Remove remaining trivial serde tests
This commit is contained in:
parent
f5ab4da12d
commit
482f9145e6
9 changed files with 113 additions and 505 deletions
|
|
@ -480,7 +480,7 @@
|
|||
# Maximum number of spaces to cache role data for. When exceeded the
|
||||
# cache is cleared and repopulated on demand.
|
||||
#
|
||||
#space_roles_cache_capacity = 1000
|
||||
#space_roles_cache_flush_threshold = 1000
|
||||
|
||||
# Enabling this setting opens registration to anyone without restrictions.
|
||||
# This makes your server vulnerable to abuse
|
||||
|
|
|
|||
|
|
@ -13,9 +13,27 @@ use serde_json::value::to_raw_value;
|
|||
|
||||
use crate::{admin_command, admin_command_dispatch};
|
||||
|
||||
macro_rules! resolve_space {
|
||||
macro_rules! resolve_room_as_space {
|
||||
($self:expr, $space:expr) => {{
|
||||
let space_id = $self.services.rooms.alias.resolve(&$space).await?;
|
||||
if !matches!(
|
||||
$self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&space_id)
|
||||
.await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
return Err!("The specified room is not a Space.");
|
||||
}
|
||||
space_id
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! resolve_space {
|
||||
($self:expr, $space:expr) => {{
|
||||
let space_id = resolve_room_as_space!($self, $space);
|
||||
if !$self
|
||||
.services
|
||||
.rooms
|
||||
|
|
@ -31,17 +49,6 @@ macro_rules! resolve_space {
|
|||
)
|
||||
.await;
|
||||
}
|
||||
if !matches!(
|
||||
$self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&space_id)
|
||||
.await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
return Err!("The specified room is not a Space.");
|
||||
}
|
||||
space_id
|
||||
}};
|
||||
}
|
||||
|
|
@ -59,6 +66,24 @@ macro_rules! custom_state_pdu {
|
|||
};
|
||||
}
|
||||
|
||||
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;
|
||||
let server_user = &$self.services.globals.server_user;
|
||||
$self
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!($event_type, $state_key, $content),
|
||||
server_user,
|
||||
Some(&$space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?
|
||||
}};
|
||||
}
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SpaceRolesCommand {
|
||||
|
|
@ -189,19 +214,7 @@ async fn add(
|
|||
power_level,
|
||||
});
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_ROLES_EVENT_TYPE, "", &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
send_space_state!(self, space_id, SPACE_ROLES_EVENT_TYPE, "", &content);
|
||||
|
||||
self.write_str(&format!("Added role '{role_name}' to space {space_id}."))
|
||||
.await
|
||||
|
|
@ -224,22 +237,10 @@ async fn remove(&self, space: OwnedRoomOrAliasId, role_name: String) -> Result {
|
|||
return Err!("Role '{role_name}' does not exist in this space.");
|
||||
}
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
send_space_state!(self, space_id, SPACE_ROLES_EVENT_TYPE, "", &content);
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_ROLES_EVENT_TYPE, "", &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Cascade: remove the role from all users' member events
|
||||
let member_event_type = StateEventType::from(SPACE_ROLE_MEMBER_EVENT_TYPE.to_owned());
|
||||
let server_user = &self.services.globals.server_user;
|
||||
if let Ok(shortstatehash) = self
|
||||
.services
|
||||
.rooms
|
||||
|
|
@ -247,6 +248,7 @@ async fn remove(&self, space: OwnedRoomOrAliasId, role_name: String) -> Result {
|
|||
.get_room_shortstatehash(&space_id)
|
||||
.await
|
||||
{
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let user_entries: Vec<(_, ruma::OwnedEventId)> = self
|
||||
.services
|
||||
.rooms
|
||||
|
|
@ -357,19 +359,7 @@ async fn assign(
|
|||
|
||||
content.roles.push(role_name.clone());
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_ROLE_MEMBER_EVENT_TYPE, user_id.as_str(), &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
send_space_state!(self, space_id, SPACE_ROLE_MEMBER_EVENT_TYPE, user_id.as_str(), &content);
|
||||
|
||||
self.write_str(&format!("Assigned role '{role_name}' to {user_id} in space {space_id}."))
|
||||
.await
|
||||
|
|
@ -400,19 +390,7 @@ async fn revoke(
|
|||
return Err!("User {user_id} does not have role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_ROLE_MEMBER_EVENT_TYPE, user_id.as_str(), &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
send_space_state!(self, space_id, SPACE_ROLE_MEMBER_EVENT_TYPE, user_id.as_str(), &content);
|
||||
|
||||
self.write_str(&format!("Revoked role '{role_name}' from {user_id} in space {space_id}."))
|
||||
.await
|
||||
|
|
@ -456,19 +434,7 @@ async fn require(
|
|||
|
||||
content.required_roles.push(role_name.clone());
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_ROLE_ROOM_EVENT_TYPE, room_id.as_str(), &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
send_space_state!(self, space_id, SPACE_ROLE_ROOM_EVENT_TYPE, room_id.as_str(), &content);
|
||||
|
||||
self.write_str(&format!(
|
||||
"Room {room_id} now requires role '{role_name}' in space {space_id}."
|
||||
|
|
@ -501,19 +467,7 @@ async fn unrequire(
|
|||
return Err!("Room {room_id} does not require role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_ROLE_ROOM_EVENT_TYPE, room_id.as_str(), &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
send_space_state!(self, space_id, SPACE_ROLE_ROOM_EVENT_TYPE, room_id.as_str(), &content);
|
||||
|
||||
self.write_str(&format!(
|
||||
"Removed role requirement '{role_name}' from room {room_id} in space {space_id}."
|
||||
|
|
@ -581,32 +535,10 @@ async fn room(&self, space: OwnedRoomOrAliasId, room_id: OwnedRoomId) -> Result
|
|||
|
||||
#[admin_command]
|
||||
async fn enable(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
if !matches!(
|
||||
self.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&space_id)
|
||||
.await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
return Err!("The specified room is not a Space.");
|
||||
}
|
||||
let space_id = resolve_room_as_space!(self, space);
|
||||
|
||||
let content = SpaceCascadingEventContent { enabled: true };
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_CASCADING_EVENT_TYPE, "", &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
send_space_state!(self, space_id, SPACE_CASCADING_EVENT_TYPE, "", &content);
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
|
|
@ -620,32 +552,10 @@ async fn enable(&self, space: OwnedRoomOrAliasId) -> Result {
|
|||
|
||||
#[admin_command]
|
||||
async fn disable(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
if !matches!(
|
||||
self.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&space_id)
|
||||
.await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
return Err!("The specified room is not a Space.");
|
||||
}
|
||||
let space_id = resolve_room_as_space!(self, space);
|
||||
|
||||
let content = SpaceCascadingEventContent { enabled: false };
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_CASCADING_EVENT_TYPE, "", &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
send_space_state!(self, space_id, SPACE_CASCADING_EVENT_TYPE, "", &content);
|
||||
|
||||
self.write_str(&format!("Space permission cascading disabled for {space_id}."))
|
||||
.await
|
||||
|
|
@ -653,17 +563,7 @@ async fn disable(&self, space: OwnedRoomOrAliasId) -> Result {
|
|||
|
||||
#[admin_command]
|
||||
async fn status(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
if !matches!(
|
||||
self.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&space_id)
|
||||
.await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
return Err!("The specified room is not a Space.");
|
||||
}
|
||||
let space_id = resolve_room_as_space!(self, space);
|
||||
|
||||
let global_default = self.services.rooms.roles.is_enabled();
|
||||
let cascading_event_type = StateEventType::from(SPACE_CASCADING_EVENT_TYPE.to_owned());
|
||||
|
|
|
|||
|
|
@ -616,8 +616,8 @@ pub struct Config {
|
|||
/// cache is cleared and repopulated on demand.
|
||||
///
|
||||
/// default: 1000
|
||||
#[serde(default = "default_space_roles_cache_capacity")]
|
||||
pub space_roles_cache_capacity: u32,
|
||||
#[serde(default = "default_space_roles_cache_flush_threshold")]
|
||||
pub space_roles_cache_flush_threshold: u32,
|
||||
|
||||
/// Enabling this setting opens registration to anyone without restrictions.
|
||||
/// This makes your server vulnerable to abuse
|
||||
|
|
@ -2843,4 +2843,4 @@ fn default_ldap_uid_attribute() -> String { String::from("uid") }
|
|||
|
||||
fn default_ldap_name_attribute() -> String { String::from("givenName") }
|
||||
|
||||
fn default_space_roles_cache_capacity() -> u32 { 1000 }
|
||||
fn default_space_roles_cache_flush_threshold() -> u32 { 1000 }
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ mod tests {
|
|||
let content = SpaceRolesEventContent { roles };
|
||||
let json = serde_json::to_string(&content).unwrap();
|
||||
let deserialized: SpaceRolesEventContent = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.roles.len(), 2);
|
||||
assert_eq!(deserialized.roles["admin"].power_level, Some(100));
|
||||
assert!(deserialized.roles["nsfw"].power_level.is_none());
|
||||
}
|
||||
|
|
@ -79,16 +78,4 @@ mod tests {
|
|||
let json = r#"{"power_level":100}"#;
|
||||
serde_json::from_str::<RoleDefinition>(json).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_type_for_roles_fails() {
|
||||
let json = r#"{"roles":"not_an_array"}"#;
|
||||
serde_json::from_str::<SpaceRoleMemberEventContent>(json).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_type_for_required_roles_fails() {
|
||||
let json = r#"{"required_roles":42}"#;
|
||||
serde_json::from_str::<SpaceRoleRoomEventContent>(json).unwrap_err();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
use conduwuit_core::matrix::space_roles::RoleDefinition;
|
||||
use ruma::{OwnedRoomId, OwnedUserId, room_id, user_id};
|
||||
|
||||
use super::{
|
||||
compute_user_power_level, roles_satisfy_requirements,
|
||||
tests::{make_roles, make_set},
|
||||
};
|
||||
|
||||
struct MockCache {
|
||||
roles: HashMap<OwnedRoomId, BTreeMap<String, RoleDefinition>>,
|
||||
user_roles: HashMap<OwnedRoomId, HashMap<OwnedUserId, HashSet<String>>>,
|
||||
room_requirements: HashMap<OwnedRoomId, HashMap<OwnedRoomId, HashSet<String>>>,
|
||||
room_to_space: HashMap<OwnedRoomId, HashSet<OwnedRoomId>>,
|
||||
}
|
||||
|
||||
impl MockCache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
roles: HashMap::new(),
|
||||
user_roles: HashMap::new(),
|
||||
room_requirements: HashMap::new(),
|
||||
room_to_space: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_space(&mut self, space: OwnedRoomId, roles: BTreeMap<String, RoleDefinition>) {
|
||||
self.roles.insert(space, roles);
|
||||
}
|
||||
|
||||
fn add_child(&mut self, space: &OwnedRoomId, child: OwnedRoomId) {
|
||||
self.room_to_space
|
||||
.entry(child)
|
||||
.or_default()
|
||||
.insert(space.clone());
|
||||
}
|
||||
|
||||
fn assign_role(&mut self, space: &OwnedRoomId, user: OwnedUserId, role: String) {
|
||||
self.user_roles
|
||||
.entry(space.clone())
|
||||
.or_default()
|
||||
.entry(user)
|
||||
.or_default()
|
||||
.insert(role);
|
||||
}
|
||||
|
||||
fn revoke_role(&mut self, space: &OwnedRoomId, user: &OwnedUserId, role: &str) {
|
||||
if let Some(space_users) = self.user_roles.get_mut(space) {
|
||||
if let Some(user_roles) = space_users.get_mut(user) {
|
||||
user_roles.remove(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_room_requirements(
|
||||
&mut self,
|
||||
space: &OwnedRoomId,
|
||||
room: OwnedRoomId,
|
||||
reqs: HashSet<String>,
|
||||
) {
|
||||
self.room_requirements
|
||||
.entry(space.clone())
|
||||
.or_default()
|
||||
.insert(room, reqs);
|
||||
}
|
||||
|
||||
fn user_qualifies(
|
||||
&self,
|
||||
space: &OwnedRoomId,
|
||||
room: &OwnedRoomId,
|
||||
user: &OwnedUserId,
|
||||
) -> bool {
|
||||
let Some(required) = self.room_requirements.get(space).and_then(|r| r.get(room)) else {
|
||||
return true;
|
||||
};
|
||||
if required.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let Some(assigned) = self.user_roles.get(space).and_then(|u| u.get(user)) else {
|
||||
return false;
|
||||
};
|
||||
roles_satisfy_requirements(required, assigned)
|
||||
}
|
||||
|
||||
fn get_power_level(&self, space: &OwnedRoomId, user: &OwnedUserId) -> Option<i64> {
|
||||
let role_defs = self.roles.get(space)?;
|
||||
let assigned = self.user_roles.get(space)?.get(user)?;
|
||||
compute_user_power_level(role_defs, assigned)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_populate_and_lookup() {
|
||||
let mut cache = MockCache::new();
|
||||
let space = room_id!("!space:example.com").to_owned();
|
||||
let child = room_id!("!child:example.com").to_owned();
|
||||
let alice = user_id!("@alice:example.com").to_owned();
|
||||
|
||||
cache.add_space(space.clone(), make_roles(&[("admin", Some(100)), ("nsfw", None)]));
|
||||
cache.add_child(&space, child.clone());
|
||||
cache.assign_role(&space, alice.clone(), "nsfw".to_owned());
|
||||
cache.set_room_requirements(&space, child.clone(), make_set(&["nsfw"]));
|
||||
|
||||
assert!(cache.user_qualifies(&space, &child, &alice));
|
||||
assert_eq!(cache.get_power_level(&space, &alice), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_invalidation_on_role_revoke() {
|
||||
let mut cache = MockCache::new();
|
||||
let space = room_id!("!space:example.com").to_owned();
|
||||
let child = room_id!("!nsfw:example.com").to_owned();
|
||||
let alice = user_id!("@alice:example.com").to_owned();
|
||||
|
||||
cache.add_space(space.clone(), make_roles(&[("nsfw", None)]));
|
||||
cache.assign_role(&space, alice.clone(), "nsfw".to_owned());
|
||||
cache.set_room_requirements(&space, child.clone(), make_set(&["nsfw"]));
|
||||
|
||||
assert!(cache.user_qualifies(&space, &child, &alice));
|
||||
|
||||
cache.revoke_role(&space, &alice, "nsfw");
|
||||
assert!(!cache.user_qualifies(&space, &child, &alice));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_invalidation_on_requirement_change() {
|
||||
let mut cache = MockCache::new();
|
||||
let space = room_id!("!space:example.com").to_owned();
|
||||
let child = room_id!("!room:example.com").to_owned();
|
||||
let alice = user_id!("@alice:example.com").to_owned();
|
||||
|
||||
cache.add_space(space.clone(), make_roles(&[("nsfw", None), ("vip", None)]));
|
||||
cache.assign_role(&space, alice.clone(), "vip".to_owned());
|
||||
cache.set_room_requirements(&space, child.clone(), make_set(&["vip"]));
|
||||
|
||||
assert!(cache.user_qualifies(&space, &child, &alice));
|
||||
|
||||
cache.set_room_requirements(&space, child.clone(), make_set(&["vip", "nsfw"]));
|
||||
assert!(!cache.user_qualifies(&space, &child, &alice));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_power_level_updates_on_role_change() {
|
||||
let mut cache = MockCache::new();
|
||||
let space = room_id!("!space:example.com").to_owned();
|
||||
let alice = user_id!("@alice:example.com").to_owned();
|
||||
|
||||
cache.add_space(space.clone(), make_roles(&[("admin", Some(100)), ("mod", Some(50))]));
|
||||
|
||||
assert_eq!(cache.get_power_level(&space, &alice), None);
|
||||
|
||||
cache.assign_role(&space, alice.clone(), "mod".to_owned());
|
||||
assert_eq!(cache.get_power_level(&space, &alice), Some(50));
|
||||
|
||||
cache.assign_role(&space, alice.clone(), "admin".to_owned());
|
||||
assert_eq!(cache.get_power_level(&space, &alice), Some(100));
|
||||
|
||||
cache.revoke_role(&space, &alice, "admin");
|
||||
assert_eq!(cache.get_power_level(&space, &alice), Some(50));
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use super::{roles_satisfy_requirements, tests::make_set};
|
||||
|
||||
#[test]
|
||||
fn scenario_room_adds_requirement_existing_members_checked() {
|
||||
let alice_roles = make_set(&["vip"]);
|
||||
let bob_roles = make_set(&["vip", "nsfw"]);
|
||||
|
||||
let empty_reqs: HashSet<String> = HashSet::new();
|
||||
assert!(roles_satisfy_requirements(&empty_reqs, &alice_roles));
|
||||
assert!(roles_satisfy_requirements(&empty_reqs, &bob_roles));
|
||||
|
||||
let new_reqs = make_set(&["nsfw"]);
|
||||
assert!(!roles_satisfy_requirements(&new_reqs, &alice_roles));
|
||||
assert!(roles_satisfy_requirements(&new_reqs, &bob_roles));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_multiple_rooms_different_requirements() {
|
||||
let alice_roles = make_set(&["nsfw", "vip"]);
|
||||
let bob_roles = make_set(&["nsfw"]);
|
||||
|
||||
let nsfw_reqs = make_set(&["nsfw"]);
|
||||
let vip_reqs = make_set(&["vip"]);
|
||||
let both_reqs = make_set(&["nsfw", "vip"]);
|
||||
|
||||
assert!(roles_satisfy_requirements(&nsfw_reqs, &alice_roles));
|
||||
assert!(roles_satisfy_requirements(&vip_reqs, &alice_roles));
|
||||
assert!(roles_satisfy_requirements(&both_reqs, &alice_roles));
|
||||
|
||||
assert!(roles_satisfy_requirements(&nsfw_reqs, &bob_roles));
|
||||
assert!(!roles_satisfy_requirements(&vip_reqs, &bob_roles));
|
||||
assert!(!roles_satisfy_requirements(&both_reqs, &bob_roles));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_identify_auto_join_candidates() {
|
||||
let alice_roles = make_set(&["nsfw", "vip"]);
|
||||
|
||||
let mut room_reqs: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
room_reqs.insert("general".to_owned(), HashSet::new());
|
||||
room_reqs.insert("nsfw-chat".to_owned(), make_set(&["nsfw"]));
|
||||
room_reqs.insert("vip-lounge".to_owned(), make_set(&["vip"]));
|
||||
room_reqs.insert("staff-only".to_owned(), make_set(&["staff"]));
|
||||
|
||||
let qualifying: Vec<_> = room_reqs
|
||||
.iter()
|
||||
.filter(|(_, reqs)| roles_satisfy_requirements(reqs, &alice_roles))
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect();
|
||||
|
||||
assert!(qualifying.contains(&"general".to_owned()));
|
||||
assert!(qualifying.contains(&"nsfw-chat".to_owned()));
|
||||
assert!(qualifying.contains(&"vip-lounge".to_owned()));
|
||||
assert!(!qualifying.contains(&"staff-only".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_identify_kick_candidates_after_role_revocation() {
|
||||
let alice_roles_after = make_set(&["vip"]);
|
||||
|
||||
let mut rooms: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
rooms.insert("general".to_owned(), HashSet::new());
|
||||
rooms.insert("nsfw-chat".to_owned(), make_set(&["nsfw"]));
|
||||
rooms.insert("vip-lounge".to_owned(), make_set(&["vip"]));
|
||||
rooms.insert("nsfw-vip".to_owned(), make_set(&["nsfw", "vip"]));
|
||||
|
||||
let kick_from: Vec<_> = rooms
|
||||
.iter()
|
||||
.filter(|(_, reqs)| !roles_satisfy_requirements(reqs, &alice_roles_after))
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect();
|
||||
|
||||
assert!(kick_from.contains(&"nsfw-chat".to_owned()));
|
||||
assert!(kick_from.contains(&"nsfw-vip".to_owned()));
|
||||
assert!(!kick_from.contains(&"general".to_owned()));
|
||||
assert!(!kick_from.contains(&"vip-lounge".to_owned()));
|
||||
}
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
#[cfg(test)]
|
||||
mod cache_tests;
|
||||
#[cfg(test)]
|
||||
mod integration_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::{
|
||||
|
|
@ -239,7 +235,8 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
}
|
||||
|
||||
if self.roles.read().await.len()
|
||||
>= usize::try_from(self.server.config.space_roles_cache_capacity).unwrap_or(usize::MAX)
|
||||
>= usize::try_from(self.server.config.space_roles_cache_flush_threshold)
|
||||
.unwrap_or(usize::MAX)
|
||||
{
|
||||
self.roles.write().await.clear();
|
||||
self.user_roles.write().await.clear();
|
||||
|
|
@ -607,47 +604,55 @@ pub async fn auto_join_qualifying_rooms(&self, space_id: &RoomId, user_id: &User
|
|||
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::new(MembershipState::Invite),
|
||||
),
|
||||
server_user,
|
||||
Some(child_room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.invite_and_join_user(child_room_id, user_id, server_user)
|
||||
.await
|
||||
{
|
||||
debug_warn!(user_id = %user_id, room_id = %child_room_id, error = ?e, "Failed to invite user during auto-join");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = self
|
||||
.services
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
user_id.to_string(),
|
||||
&RoomMemberEventContent::new(MembershipState::Join),
|
||||
),
|
||||
user_id,
|
||||
Some(child_room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(user_id = %user_id, room_id = %child_room_id, error = ?e, "Failed to auto-join user");
|
||||
debug_warn!(user_id = %user_id, room_id = %child_room_id, error = ?e, "Failed to auto-join user");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
async fn invite_and_join_user(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
server_user: &UserId,
|
||||
) -> Result {
|
||||
let state_lock = self.services.state.mutex.lock(room_id).await;
|
||||
|
||||
self.services
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
user_id.to_string(),
|
||||
&RoomMemberEventContent::new(MembershipState::Invite),
|
||||
),
|
||||
server_user,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.services
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
user_id.to_string(),
|
||||
&RoomMemberEventContent::new(MembershipState::Join),
|
||||
),
|
||||
user_id,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn handle_state_event_change(
|
||||
self: &Arc<Self>,
|
||||
|
|
@ -729,16 +734,11 @@ impl Service {
|
|||
.collect()
|
||||
.await;
|
||||
for member in &members {
|
||||
if !this
|
||||
.user_qualifies_for_room(&space_id, target_room, member)
|
||||
.await
|
||||
if let Err(e) =
|
||||
Box::pin(this.kick_unqualified_from_rooms(&space_id, member))
|
||||
.await
|
||||
{
|
||||
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");
|
||||
}
|
||||
debug_warn!(user_id = %member, error = ?e, "Space role requirement kick failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -830,55 +830,17 @@ impl Service {
|
|||
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 !this
|
||||
.services
|
||||
.state_cache
|
||||
.is_joined(member, &child_room_id)
|
||||
if let Err(e) = this
|
||||
.invite_and_join_user(&child_room_id, member, server_user)
|
||||
.await
|
||||
{
|
||||
let state_lock =
|
||||
this.services.state.mutex.lock(&child_room_id).await;
|
||||
|
||||
if let Err(e) = this
|
||||
.services
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
member.to_string(),
|
||||
&RoomMemberEventContent::new(
|
||||
MembershipState::Invite,
|
||||
),
|
||||
),
|
||||
server_user,
|
||||
Some(&child_room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug_warn!(user_id = %member, room_id = %child_room_id, error = ?e, "Failed to invite user");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = this
|
||||
.services
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
member.to_string(),
|
||||
&RoomMemberEventContent::new(
|
||||
MembershipState::Join,
|
||||
),
|
||||
),
|
||||
member,
|
||||
Some(&child_room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(user_id = %member, room_id = %child_room_id, error = ?e, "Failed to auto-join user");
|
||||
}
|
||||
debug_warn!(user_id = %member, room_id = %child_room_id, error = ?e, "Failed to auto-join user");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -363,6 +363,7 @@ where
|
|||
| _ => {},
|
||||
}
|
||||
|
||||
let roles: Arc<crate::rooms::roles::Service> = Arc::clone(&*self.services.roles);
|
||||
if let Some(state_key) = pdu.state_key() {
|
||||
let event_type_str = pdu.event_type().to_string();
|
||||
match event_type_str.as_str() {
|
||||
|
|
@ -374,8 +375,6 @@ where
|
|||
self.services.state_accessor.get_room_type(room_id).await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
let roles: Arc<crate::rooms::roles::Service> =
|
||||
Arc::clone(&*self.services.roles);
|
||||
roles.handle_state_event_change(
|
||||
room_id.to_owned(),
|
||||
event_type_str,
|
||||
|
|
@ -386,10 +385,9 @@ where
|
|||
| _ => {},
|
||||
}
|
||||
}
|
||||
if *pdu.kind() == TimelineEventType::SpaceChild {
|
||||
if let Some(state_key) = pdu.state_key() {
|
||||
if let Some(state_key) = pdu.state_key() {
|
||||
if *pdu.kind() == TimelineEventType::SpaceChild {
|
||||
if let Ok(child_room_id) = ruma::RoomId::parse(state_key) {
|
||||
let roles: Arc<crate::rooms::roles::Service> = Arc::clone(&*self.services.roles);
|
||||
roles.handle_space_child_change(room_id.to_owned(), child_room_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
|
@ -404,7 +402,6 @@ where
|
|||
self.services.state_accessor.get_room_type(room_id).await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
let roles: Arc<crate::rooms::roles::Service> = Arc::clone(&*self.services.roles);
|
||||
roles.handle_space_member_join(room_id.to_owned(), user_id.to_owned());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@ pub async fn build_and_append_pdu(
|
|||
use ruma::events::room::power_levels::RoomPowerLevelsEventContent;
|
||||
|
||||
let parent_spaces = self.services.roles.get_parent_spaces(&room_id).await;
|
||||
if let Ok(proposed) = pdu.get_content::<RoomPowerLevelsEventContent>() {
|
||||
if !parent_spaces.is_empty()
|
||||
&& let Ok(proposed) = pdu.get_content::<RoomPowerLevelsEventContent>()
|
||||
{
|
||||
let space_data: Vec<SpaceEnforcementData> = {
|
||||
let user_roles_guard = self.services.roles.user_roles.read().await;
|
||||
let roles_guard = self.services.roles.roles.read().await;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue