//! Types for the `m.room.join_rules` event. use ruma_events_macros::EventContent; #[cfg(feature = "unstable-pre-spec")] use ruma_identifiers::RoomId; #[cfg(feature = "unstable-pre-spec")] use serde::de::DeserializeOwned; use serde::{ de::{Deserializer, Error}, Deserialize, Serialize, }; use serde_json::value::RawValue as RawJsonValue; #[cfg(feature = "unstable-pre-spec")] use serde_json::Value as JsonValue; use std::borrow::Cow; #[cfg(feature = "unstable-pre-spec")] use std::collections::BTreeMap; /// The content of an `m.room.join_rules` event. /// /// Describes how users are allowed to join the room. #[derive(Clone, Debug, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.join_rules", kind = State)] pub struct RoomJoinRulesEventContent { /// The type of rules used for users wishing to join this room. #[ruma_event(skip_redaction)] #[serde(flatten)] pub join_rule: JoinRule, } impl RoomJoinRulesEventContent { /// Creates a new `RoomJoinRulesEventContent` with the given rule. pub fn new(join_rule: JoinRule) -> Self { Self { join_rule } } /// Creates a new `RoomJoinRulesEventContent` with the restricted rule and the given set of /// allow rules. #[cfg(feature = "unstable-pre-spec")] pub fn restricted(allow: Vec) -> Self { Self { join_rule: JoinRule::Restricted(Restricted::new(allow)) } } } impl<'de> Deserialize<'de> for RoomJoinRulesEventContent { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let join_rule = JoinRule::deserialize(deserializer)?; Ok(RoomJoinRulesEventContent { join_rule }) } } /// The rule used for users wishing to join this room. /// /// This type can hold an arbitrary string. To check for formats that are not available as a /// documented variant here, use its string representation, obtained through `.as_str()`. #[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "join_rule")] pub enum JoinRule { /// A user who wishes to join the room must first receive an invite to the room from someone /// already inside of the room. #[serde(rename = "invite")] Invite, /// Reserved but not yet implemented by the Matrix specification. #[serde(rename = "knock")] Knock, /// Reserved but not yet implemented by the Matrix specification. #[serde(rename = "private")] Private, /// Users can join the room if they are invited, or if they meet any of the conditions /// described in a set of [`AllowRule`]s. #[cfg(feature = "unstable-pre-spec")] #[serde(rename = "restricted")] Restricted(Restricted), /// Anyone can join the room without any prior action. #[serde(rename = "public")] Public, #[doc(hidden)] #[serde(skip_serializing)] _Custom(String), } impl JoinRule { /// Returns the string name of this `JoinRule` pub fn as_str(&self) -> &str { match self { JoinRule::Invite => "invite", JoinRule::Knock => "knock", JoinRule::Private => "private", #[cfg(feature = "unstable-pre-spec")] JoinRule::Restricted(_) => "restricted", JoinRule::Public => "public", JoinRule::_Custom(rule) => rule, } } } impl<'de> Deserialize<'de> for JoinRule { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[cfg(feature = "unstable-pre-spec")] fn from_raw_json_value(raw: &RawJsonValue) -> Result { serde_json::from_str(raw.get()).map_err(E::custom) } let json: Box = Box::deserialize(deserializer)?; #[derive(Deserialize)] struct ExtractType<'a> { join_rule: Option>, } let join_rule = serde_json::from_str::>(json.get()) .map_err(serde::de::Error::custom)? .join_rule .ok_or_else(|| D::Error::missing_field("join_rule"))?; match join_rule.as_ref() { "invite" => Ok(Self::Invite), "knock" => Ok(Self::Knock), "private" => Ok(Self::Private), #[cfg(feature = "unstable-pre-spec")] "restricted" => from_raw_json_value(&json).map(Self::Restricted), "public" => Ok(Self::Public), _ => Ok(Self::_Custom(join_rule.into_owned())), } } } /// Configuration of the `Restricted` join rule. #[cfg(feature = "unstable-pre-spec")] #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Restricted { /// Allow rules which describe conditions that allow joining a room. allow: Vec, } #[cfg(feature = "unstable-pre-spec")] impl Restricted { /// Constructs a new rule set for restricted rooms with the given rules. pub fn new(allow: Vec) -> Self { Self { allow } } } /// An allow rule which defines a condition that allows joining a room. #[cfg(feature = "unstable-pre-spec")] #[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type")] pub enum AllowRule { /// Joining is allowed if a user is already a member of the room with the id `room_id`. #[serde(rename = "m.room_membership")] RoomMembership(RoomMembership), #[doc(hidden)] _Custom(CustomAllowRule), } #[cfg(feature = "unstable-pre-spec")] impl AllowRule { /// Constructs an `AllowRule` with membership of the room with the given id as its predicate. pub fn room_membership(room_id: Box) -> Self { Self::RoomMembership(RoomMembership::new(room_id)) } } /// Allow rule which grants permission to join based on the membership of another room. #[cfg(feature = "unstable-pre-spec")] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RoomMembership { /// The id of the room which being a member of grants permission to join another room. pub room_id: Box, } #[cfg(feature = "unstable-pre-spec")] impl RoomMembership { /// Constructs a new room membership rule for the given room id. pub fn new(room_id: Box) -> Self { Self { room_id } } } #[cfg(feature = "unstable-pre-spec")] #[doc(hidden)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct CustomAllowRule { #[serde(rename = "type")] rule_type: String, #[serde(flatten)] extra: BTreeMap, } #[cfg(feature = "unstable-pre-spec")] impl<'de> Deserialize<'de> for AllowRule { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { fn from_raw_json_value(raw: &RawJsonValue) -> Result { serde_json::from_str(raw.get()).map_err(E::custom) } let json: Box = Box::deserialize(deserializer)?; // Extracts the `type` value. #[derive(Deserialize)] struct ExtractType<'a> { #[serde(borrow, rename = "type")] rule_type: Option>, } // Get the value of `type` if present. let rule_type = serde_json::from_str::>(json.get()) .map_err(serde::de::Error::custom)? .rule_type; match rule_type.as_deref() { Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership), Some(_) => from_raw_json_value(&json).map(Self::_Custom), None => Err(D::Error::missing_field("type")), } } } #[cfg(test)] mod tests { use matches::assert_matches; #[cfg(feature = "unstable-pre-spec")] use ruma_identifiers::room_id; #[cfg(feature = "unstable-pre-spec")] use super::AllowRule; use super::{JoinRule, RoomJoinRulesEventContent, SyncRoomJoinRulesEvent}; #[test] fn deserialize() { let json = r#"{"join_rule": "public"}"#; let event: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap(); assert_matches!(event, RoomJoinRulesEventContent { join_rule: JoinRule::Public }); } #[cfg(feature = "unstable-pre-spec")] #[test] fn deserialize_unstable() { let json = r#"{ "join_rule": "restricted", "allow": [ { "type": "m.room_membership", "room_id": "!mods:example.org" }, { "type": "m.room_membership", "room_id": "!users:example.org" } ] }"#; let event: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap(); match event.join_rule { JoinRule::Restricted(restricted) => assert_eq!( restricted.allow, &[ AllowRule::room_membership(room_id!("!mods:example.org").to_owned()), AllowRule::room_membership(room_id!("!users:example.org").to_owned()) ] ), rule => panic!("Deserialized to wrong variant: {:?}", rule), } } #[test] fn deserialize_restricted_event() { let json = r#"{ "type": "m.room.join_rules", "sender": "@admin:community.rs", "content": { "join_rule": "restricted", "allow":[ { "type": "m.room_membership","room_id": "!KqeUnzmXPIhHRaWMTs:mccarty.io" } ] }, "state_key": "", "origin_server_ts":1630508835342, "unsigned": { "age":4165521871 }, "event_id": "$0ACb9KSPlT3al3kikyRYvFhMqXPP9ZcQOBrsdIuh58U" }"#; assert_matches!(serde_json::from_str::(json), Ok(_)); } }