use std::{borrow::Cow, collections::BTreeMap};
use ruma_common::{serde::from_raw_json_value, space::SpaceRoomJoinRule, OwnedRoomId};
use ruma_macros::EventContent;
use serde::{
de::{Deserializer, Error},
Deserialize, Serialize,
};
use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue};
use crate::{EmptyStateKey, PrivOwnedStr};
#[derive(Clone, Debug, Serialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.room.join_rules", kind = State, state_key_type = EmptyStateKey)]
pub struct RoomJoinRulesEventContent {
#[ruma_event(skip_redaction)]
#[serde(flatten)]
pub join_rule: JoinRule,
}
impl RoomJoinRulesEventContent {
pub fn new(join_rule: JoinRule) -> Self {
Self { join_rule }
}
pub fn restricted(allow: Vec<AllowRule>) -> Self {
Self { join_rule: JoinRule::Restricted(Restricted::new(allow)) }
}
pub fn knock_restricted(allow: Vec<AllowRule>) -> Self {
Self { join_rule: JoinRule::KnockRestricted(Restricted::new(allow)) }
}
}
impl<'de> Deserialize<'de> for RoomJoinRulesEventContent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let join_rule = JoinRule::deserialize(deserializer)?;
Ok(RoomJoinRulesEventContent { join_rule })
}
}
impl RoomJoinRulesEvent {
pub fn join_rule(&self) -> &JoinRule {
match self {
Self::Original(ev) => &ev.content.join_rule,
Self::Redacted(ev) => &ev.content.join_rule,
}
}
}
impl SyncRoomJoinRulesEvent {
pub fn join_rule(&self) -> &JoinRule {
match self {
Self::Original(ev) => &ev.content.join_rule,
Self::Redacted(ev) => &ev.content.join_rule,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "join_rule", rename_all = "snake_case")]
pub enum JoinRule {
Invite,
Knock,
Private,
Restricted(Restricted),
KnockRestricted(Restricted),
Public,
#[doc(hidden)]
#[serde(skip_serializing)]
_Custom(PrivOwnedStr),
}
impl JoinRule {
pub fn as_str(&self) -> &str {
match self {
JoinRule::Invite => "invite",
JoinRule::Knock => "knock",
JoinRule::Private => "private",
JoinRule::Restricted(_) => "restricted",
JoinRule::KnockRestricted(_) => "knock_restricted",
JoinRule::Public => "public",
JoinRule::_Custom(rule) => &rule.0,
}
}
}
impl<'de> Deserialize<'de> for JoinRule {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
#[derive(Deserialize)]
struct ExtractType<'a> {
#[serde(borrow)]
join_rule: Option<Cow<'a, str>>,
}
let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get())
.map_err(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),
"restricted" => from_raw_json_value(&json).map(Self::Restricted),
"knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
"public" => Ok(Self::Public),
_ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
}
}
}
impl From<JoinRule> for SpaceRoomJoinRule {
fn from(value: JoinRule) -> Self {
value.as_str().into()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct Restricted {
#[serde(default)]
pub allow: Vec<AllowRule>,
}
impl Restricted {
pub fn new(allow: Vec<AllowRule>) -> Self {
Self { allow }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(untagged)]
pub enum AllowRule {
RoomMembership(RoomMembership),
#[doc(hidden)]
_Custom(Box<CustomAllowRule>),
}
impl AllowRule {
pub fn room_membership(room_id: OwnedRoomId) -> Self {
Self::RoomMembership(RoomMembership::new(room_id))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "type", rename = "m.room_membership")]
pub struct RoomMembership {
pub room_id: OwnedRoomId,
}
impl RoomMembership {
pub fn new(room_id: OwnedRoomId) -> Self {
Self { room_id }
}
}
#[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<String, JsonValue>,
}
impl<'de> Deserialize<'de> for AllowRule {
fn deserialize<D>(deserializer: D) -> Result<AllowRule, D::Error>
where
D: Deserializer<'de>,
{
let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
#[derive(Deserialize)]
struct ExtractType<'a> {
#[serde(borrow, rename = "type")]
rule_type: Option<Cow<'a, str>>,
}
let rule_type =
serde_json::from_str::<ExtractType<'_>>(json.get()).map_err(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 assert_matches2::assert_matches;
use ruma_common::owned_room_id;
use super::{
AllowRule, JoinRule, OriginalSyncRoomJoinRulesEvent, Restricted, RoomJoinRulesEventContent,
SpaceRoomJoinRule,
};
#[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 });
}
#[test]
fn deserialize_restricted() {
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(owned_room_id!("!mods:example.org")),
AllowRule::room_membership(owned_room_id!("!users:example.org"))
]
),
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::<OriginalSyncRoomJoinRulesEvent>(json), Ok(_));
}
#[test]
fn roundtrip_custom_allow_rule() {
let json = r#"{"type":"org.msc9000.something","foo":"bar"}"#;
let allow_rule: AllowRule = serde_json::from_str(json).unwrap();
assert_matches!(&allow_rule, AllowRule::_Custom(_));
assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json);
}
#[test]
fn restricted_room_no_allow_field() {
let json = r#"{"join_rule":"restricted"}"#;
let join_rules: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap();
assert_matches!(
join_rules,
RoomJoinRulesEventContent { join_rule: JoinRule::Restricted(_) }
);
}
#[test]
fn join_rule_to_space_room_join_rule() {
assert_eq!(SpaceRoomJoinRule::Invite, JoinRule::Invite.into());
assert_eq!(SpaceRoomJoinRule::Knock, JoinRule::Knock.into());
assert_eq!(
SpaceRoomJoinRule::KnockRestricted,
JoinRule::KnockRestricted(Restricted::default()).into()
);
assert_eq!(SpaceRoomJoinRule::Public, JoinRule::Public.into());
assert_eq!(SpaceRoomJoinRule::Private, JoinRule::Private.into());
assert_eq!(
SpaceRoomJoinRule::Restricted,
JoinRule::Restricted(Restricted::default()).into()
);
}
}