ruma_events/room/
join_rules.rs

1//! Types for the [`m.room.join_rules`] event.
2//!
3//! [`m.room.join_rules`]: https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules
4
5use std::{borrow::Cow, collections::BTreeMap};
6
7use ruma_common::{serde::from_raw_json_value, space::SpaceRoomJoinRule, OwnedRoomId};
8use ruma_macros::EventContent;
9use serde::{
10    de::{Deserializer, Error},
11    Deserialize, Serialize,
12};
13use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue};
14
15use crate::{EmptyStateKey, PrivOwnedStr};
16
17/// The content of an `m.room.join_rules` event.
18///
19/// Describes how users are allowed to join the room.
20#[derive(Clone, Debug, Serialize, EventContent)]
21#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
22#[ruma_event(type = "m.room.join_rules", kind = State, state_key_type = EmptyStateKey)]
23pub struct RoomJoinRulesEventContent {
24    /// The type of rules used for users wishing to join this room.
25    #[ruma_event(skip_redaction)]
26    #[serde(flatten)]
27    pub join_rule: JoinRule,
28}
29
30impl RoomJoinRulesEventContent {
31    /// Creates a new `RoomJoinRulesEventContent` with the given rule.
32    pub fn new(join_rule: JoinRule) -> Self {
33        Self { join_rule }
34    }
35
36    /// Creates a new `RoomJoinRulesEventContent` with the restricted rule and the given set of
37    /// allow rules.
38    pub fn restricted(allow: Vec<AllowRule>) -> Self {
39        Self { join_rule: JoinRule::Restricted(Restricted::new(allow)) }
40    }
41
42    /// Creates a new `RoomJoinRulesEventContent` with the knock restricted rule and the given set
43    /// of allow rules.
44    pub fn knock_restricted(allow: Vec<AllowRule>) -> Self {
45        Self { join_rule: JoinRule::KnockRestricted(Restricted::new(allow)) }
46    }
47}
48
49impl<'de> Deserialize<'de> for RoomJoinRulesEventContent {
50    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        let join_rule = JoinRule::deserialize(deserializer)?;
55        Ok(RoomJoinRulesEventContent { join_rule })
56    }
57}
58
59impl RoomJoinRulesEvent {
60    /// Obtain the join rule, regardless of whether this event is redacted.
61    pub fn join_rule(&self) -> &JoinRule {
62        match self {
63            Self::Original(ev) => &ev.content.join_rule,
64            Self::Redacted(ev) => &ev.content.join_rule,
65        }
66    }
67}
68
69impl SyncRoomJoinRulesEvent {
70    /// Obtain the join rule, regardless of whether this event is redacted.
71    pub fn join_rule(&self) -> &JoinRule {
72        match self {
73            Self::Original(ev) => &ev.content.join_rule,
74            Self::Redacted(ev) => &ev.content.join_rule,
75        }
76    }
77}
78
79/// The rule used for users wishing to join this room.
80///
81/// This type can hold an arbitrary string. To check for values that are not available as a
82/// documented variant here, use its string representation, obtained through `.as_str()`.
83#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
84#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
85#[serde(tag = "join_rule", rename_all = "snake_case")]
86pub enum JoinRule {
87    /// A user who wishes to join the room must first receive an invite to the room from someone
88    /// already inside of the room.
89    Invite,
90
91    /// Users can join the room if they are invited, or they can request an invite to the room.
92    ///
93    /// They can be allowed (invited) or denied (kicked/banned) access.
94    Knock,
95
96    /// Reserved but not yet implemented by the Matrix specification.
97    Private,
98
99    /// Users can join the room if they are invited, or if they meet any of the conditions
100    /// described in a set of [`AllowRule`]s.
101    Restricted(Restricted),
102
103    /// Users can join the room if they are invited, or if they meet any of the conditions
104    /// described in a set of [`AllowRule`]s, or they can request an invite to the room.
105    KnockRestricted(Restricted),
106
107    /// Anyone can join the room without any prior action.
108    Public,
109
110    #[doc(hidden)]
111    #[serde(skip_serializing)]
112    _Custom(PrivOwnedStr),
113}
114
115impl JoinRule {
116    /// Returns the string name of this `JoinRule`
117    pub fn as_str(&self) -> &str {
118        match self {
119            JoinRule::Invite => "invite",
120            JoinRule::Knock => "knock",
121            JoinRule::Private => "private",
122            JoinRule::Restricted(_) => "restricted",
123            JoinRule::KnockRestricted(_) => "knock_restricted",
124            JoinRule::Public => "public",
125            JoinRule::_Custom(rule) => &rule.0,
126        }
127    }
128}
129
130impl<'de> Deserialize<'de> for JoinRule {
131    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132    where
133        D: Deserializer<'de>,
134    {
135        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
136
137        #[derive(Deserialize)]
138        struct ExtractType<'a> {
139            #[serde(borrow)]
140            join_rule: Option<Cow<'a, str>>,
141        }
142
143        let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get())
144            .map_err(Error::custom)?
145            .join_rule
146            .ok_or_else(|| D::Error::missing_field("join_rule"))?;
147
148        match join_rule.as_ref() {
149            "invite" => Ok(Self::Invite),
150            "knock" => Ok(Self::Knock),
151            "private" => Ok(Self::Private),
152            "restricted" => from_raw_json_value(&json).map(Self::Restricted),
153            "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
154            "public" => Ok(Self::Public),
155            _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
156        }
157    }
158}
159
160impl From<JoinRule> for SpaceRoomJoinRule {
161    fn from(value: JoinRule) -> Self {
162        value.as_str().into()
163    }
164}
165
166/// Configuration of the `Restricted` join rule.
167#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
168#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
169pub struct Restricted {
170    /// Allow rules which describe conditions that allow joining a room.
171    #[serde(default)]
172    pub allow: Vec<AllowRule>,
173}
174
175impl Restricted {
176    /// Constructs a new rule set for restricted rooms with the given rules.
177    pub fn new(allow: Vec<AllowRule>) -> Self {
178        Self { allow }
179    }
180}
181
182/// An allow rule which defines a condition that allows joining a room.
183#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
184#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
185#[serde(untagged)]
186pub enum AllowRule {
187    /// Joining is allowed if a user is already a member of the room with the id `room_id`.
188    RoomMembership(RoomMembership),
189
190    #[doc(hidden)]
191    _Custom(Box<CustomAllowRule>),
192}
193
194impl AllowRule {
195    /// Constructs an `AllowRule` with membership of the room with the given id as its predicate.
196    pub fn room_membership(room_id: OwnedRoomId) -> Self {
197        Self::RoomMembership(RoomMembership::new(room_id))
198    }
199}
200
201/// Allow rule which grants permission to join based on the membership of another room.
202#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
203#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
204#[serde(tag = "type", rename = "m.room_membership")]
205pub struct RoomMembership {
206    /// The id of the room which being a member of grants permission to join another room.
207    pub room_id: OwnedRoomId,
208}
209
210impl RoomMembership {
211    /// Constructs a new room membership rule for the given room id.
212    pub fn new(room_id: OwnedRoomId) -> Self {
213        Self { room_id }
214    }
215}
216
217#[doc(hidden)]
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
220pub struct CustomAllowRule {
221    #[serde(rename = "type")]
222    rule_type: String,
223    #[serde(flatten)]
224    extra: BTreeMap<String, JsonValue>,
225}
226
227impl<'de> Deserialize<'de> for AllowRule {
228    fn deserialize<D>(deserializer: D) -> Result<AllowRule, D::Error>
229    where
230        D: Deserializer<'de>,
231    {
232        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
233
234        // Extracts the `type` value.
235        #[derive(Deserialize)]
236        struct ExtractType<'a> {
237            #[serde(borrow, rename = "type")]
238            rule_type: Option<Cow<'a, str>>,
239        }
240
241        // Get the value of `type` if present.
242        let rule_type =
243            serde_json::from_str::<ExtractType<'_>>(json.get()).map_err(Error::custom)?.rule_type;
244
245        match rule_type.as_deref() {
246            Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership),
247            Some(_) => from_raw_json_value(&json).map(Self::_Custom),
248            None => Err(D::Error::missing_field("type")),
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use assert_matches2::assert_matches;
256    use ruma_common::owned_room_id;
257
258    use super::{
259        AllowRule, JoinRule, OriginalSyncRoomJoinRulesEvent, Restricted, RoomJoinRulesEventContent,
260        SpaceRoomJoinRule,
261    };
262
263    #[test]
264    fn deserialize() {
265        let json = r#"{"join_rule": "public"}"#;
266        let event: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap();
267        assert_matches!(event, RoomJoinRulesEventContent { join_rule: JoinRule::Public });
268    }
269
270    #[test]
271    fn deserialize_restricted() {
272        let json = r#"{
273            "join_rule": "restricted",
274            "allow": [
275                {
276                    "type": "m.room_membership",
277                    "room_id": "!mods:example.org"
278                },
279                {
280                    "type": "m.room_membership",
281                    "room_id": "!users:example.org"
282                }
283            ]
284        }"#;
285        let event: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap();
286        match event.join_rule {
287            JoinRule::Restricted(restricted) => assert_eq!(
288                restricted.allow,
289                &[
290                    AllowRule::room_membership(owned_room_id!("!mods:example.org")),
291                    AllowRule::room_membership(owned_room_id!("!users:example.org"))
292                ]
293            ),
294            rule => panic!("Deserialized to wrong variant: {rule:?}"),
295        }
296    }
297
298    #[test]
299    fn deserialize_restricted_event() {
300        let json = r#"{
301            "type": "m.room.join_rules",
302            "sender": "@admin:community.rs",
303            "content": {
304                "join_rule": "restricted",
305                "allow": [
306                    { "type": "m.room_membership","room_id": "!KqeUnzmXPIhHRaWMTs:mccarty.io" }
307                ]
308            },
309            "state_key": "",
310            "origin_server_ts":1630508835342,
311            "unsigned": {
312                "age":4165521871
313            },
314            "event_id": "$0ACb9KSPlT3al3kikyRYvFhMqXPP9ZcQOBrsdIuh58U"
315        }"#;
316
317        assert_matches!(serde_json::from_str::<OriginalSyncRoomJoinRulesEvent>(json), Ok(_));
318    }
319
320    #[test]
321    fn roundtrip_custom_allow_rule() {
322        let json = r#"{"type":"org.msc9000.something","foo":"bar"}"#;
323        let allow_rule: AllowRule = serde_json::from_str(json).unwrap();
324        assert_matches!(&allow_rule, AllowRule::_Custom(_));
325        assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json);
326    }
327
328    #[test]
329    fn restricted_room_no_allow_field() {
330        let json = r#"{"join_rule":"restricted"}"#;
331        let join_rules: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap();
332        assert_matches!(
333            join_rules,
334            RoomJoinRulesEventContent { join_rule: JoinRule::Restricted(_) }
335        );
336    }
337
338    #[test]
339    fn join_rule_to_space_room_join_rule() {
340        assert_eq!(SpaceRoomJoinRule::Invite, JoinRule::Invite.into());
341        assert_eq!(SpaceRoomJoinRule::Knock, JoinRule::Knock.into());
342        assert_eq!(
343            SpaceRoomJoinRule::KnockRestricted,
344            JoinRule::KnockRestricted(Restricted::default()).into()
345        );
346        assert_eq!(SpaceRoomJoinRule::Public, JoinRule::Public.into());
347        assert_eq!(SpaceRoomJoinRule::Private, JoinRule::Private.into());
348        assert_eq!(
349            SpaceRoomJoinRule::Restricted,
350            JoinRule::Restricted(Restricted::default()).into()
351        );
352    }
353}