feat(spaces): add custom state event types for space roles

Define serde content types for m.space.roles, m.space.role.member,
and m.space.role.room custom state events used by space permission
cascading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ember33 2026-03-17 16:45:50 +01:00
parent dc8949f4d1
commit c5ffc4963c
2 changed files with 101 additions and 0 deletions

View file

@ -2,6 +2,7 @@
pub mod event;
pub mod pdu;
pub mod space_roles;
pub mod state_key;
pub mod state_res;

View file

@ -0,0 +1,100 @@
//! Custom state event content types for space permission cascading.
//!
//! These events live in Space rooms and define roles, user-role assignments,
//! and room-role requirements.
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
/// Content for `m.space.roles` (state key: "")
///
/// Defines available roles for a Space.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SpaceRolesEventContent {
pub roles: BTreeMap<String, RoleDefinition>,
}
/// A single role definition within a Space.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RoleDefinition {
pub description: String,
/// If present, users with this role receive this power level in child
/// rooms.
#[serde(skip_serializing_if = "Option::is_none")]
pub power_level: Option<i64>,
}
/// Content for `m.space.role.member` (state key: user ID)
///
/// Assigns roles to a user within a Space.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SpaceRoleMemberEventContent {
pub roles: Vec<String>,
}
/// Content for `m.space.role.room` (state key: room ID)
///
/// Declares which roles a child room requires for access.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SpaceRoleRoomEventContent {
pub required_roles: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serialize_space_roles() {
let mut roles = BTreeMap::new();
roles.insert(
"admin".to_owned(),
RoleDefinition {
description: "Space administrator".to_owned(),
power_level: Some(100),
},
);
roles.insert(
"nsfw".to_owned(),
RoleDefinition {
description: "NSFW access".to_owned(),
power_level: None,
},
);
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());
}
#[test]
fn serialize_role_member() {
let content = SpaceRoleMemberEventContent {
roles: vec!["nsfw".to_owned(), "vip".to_owned()],
};
let json = serde_json::to_string(&content).unwrap();
let deserialized: SpaceRoleMemberEventContent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.roles, vec!["nsfw", "vip"]);
}
#[test]
fn serialize_role_room() {
let content = SpaceRoleRoomEventContent {
required_roles: vec!["nsfw".to_owned()],
};
let json = serde_json::to_string(&content).unwrap();
let deserialized: SpaceRoleRoomEventContent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.required_roles, vec!["nsfw"]);
}
#[test]
fn empty_roles_deserialize() {
let json = r#"{"roles":{}}"#;
let content: SpaceRolesEventContent = serde_json::from_str(json).unwrap();
assert!(content.roles.is_empty());
}
}