Skip to main content

ruma_common/
room.rs

1//! Common types for rooms.
2
3use std::borrow::Cow;
4
5use as_variant::as_variant;
6use js_int::UInt;
7use serde::{Deserialize, Serialize, de};
8use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
9
10use crate::{
11    EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr,
12    RoomVersionId,
13    serde::{JsonObject, StringEnum, from_raw_json_value},
14};
15
16/// An enum of possible room types.
17#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
18#[derive(Clone, StringEnum)]
19#[non_exhaustive]
20pub enum RoomType {
21    /// Defines the room as a space.
22    #[ruma_enum(rename = "m.space")]
23    Space,
24
25    /// Defines the room as a call.
26    ///
27    /// This uses the unstable prefix in
28    /// [MSC3417](https://github.com/matrix-org/matrix-spec-proposals/pull/3417).
29    #[cfg(feature = "unstable-msc3417")]
30    #[ruma_enum(rename = "org.matrix.msc3417.call")]
31    Call,
32
33    /// Defines the room as a custom type.
34    #[doc(hidden)]
35    _Custom(PrivOwnedStr),
36}
37
38/// The rule used for users wishing to join this room.
39///
40/// This type can hold an arbitrary join rule. To check for values that are not available as a
41/// documented variant here, get its kind with [`.kind()`](Self::kind) or its string representation
42/// with [`.as_str()`](Self::as_str), and its associated data with [`.data()`](Self::data).
43#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
44#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
45#[serde(tag = "join_rule", rename_all = "snake_case")]
46pub enum JoinRule {
47    /// A user who wishes to join the room must first receive an invite to the room from someone
48    /// already inside of the room.
49    Invite,
50
51    /// Users can join the room if they are invited, or they can request an invite to the room.
52    ///
53    /// They can be allowed (invited) or denied (kicked/banned) access.
54    Knock,
55
56    /// Reserved but not yet implemented by the Matrix specification.
57    Private,
58
59    /// Users can join the room if they are invited, or if they meet any of the conditions
60    /// described in a set of [`AllowRule`]s.
61    Restricted(Restricted),
62
63    /// Users can join the room if they are invited, or if they meet any of the conditions
64    /// described in a set of [`AllowRule`]s, or they can request an invite to the room.
65    KnockRestricted(Restricted),
66
67    /// Anyone can join the room without any prior action.
68    Public,
69
70    #[doc(hidden)]
71    #[serde(untagged)]
72    _Custom(CustomJoinRule),
73}
74
75impl JoinRule {
76    /// Returns the kind of this `JoinRule`.
77    pub fn kind(&self) -> JoinRuleKind {
78        match self {
79            Self::Invite => JoinRuleKind::Invite,
80            Self::Knock => JoinRuleKind::Knock,
81            Self::Private => JoinRuleKind::Private,
82            Self::Restricted(_) => JoinRuleKind::Restricted,
83            Self::KnockRestricted(_) => JoinRuleKind::KnockRestricted,
84            Self::Public => JoinRuleKind::Public,
85            Self::_Custom(CustomJoinRule { join_rule, .. }) => {
86                JoinRuleKind::_Custom(PrivOwnedStr(join_rule.as_str().into()))
87            }
88        }
89    }
90
91    /// Returns the string name of this `JoinRule`
92    pub fn as_str(&self) -> &str {
93        match self {
94            JoinRule::Invite => "invite",
95            JoinRule::Knock => "knock",
96            JoinRule::Private => "private",
97            JoinRule::Restricted(_) => "restricted",
98            JoinRule::KnockRestricted(_) => "knock_restricted",
99            JoinRule::Public => "public",
100            JoinRule::_Custom(CustomJoinRule { join_rule, .. }) => join_rule,
101        }
102    }
103
104    /// Returns the associated data of this `JoinRule`.
105    ///
106    /// The returned JSON object won't contain the `join_rule` field, use
107    /// [`.kind()`](Self::kind) or [`.as_str()`](Self::as_str) to access those.
108    ///
109    /// Prefer to use the public variants of `JoinRule` where possible; this method is meant to
110    /// be used for custom join rules only.
111    pub fn data(&self) -> Cow<'_, JsonObject> {
112        fn serialize<T: Serialize>(obj: &T) -> JsonObject {
113            match serde_json::to_value(obj).expect("join rule serialization should succeed") {
114                JsonValue::Object(mut obj) => {
115                    obj.remove("body");
116                    obj
117                }
118                _ => panic!("all message types should serialize to objects"),
119            }
120        }
121
122        match self {
123            JoinRule::Invite | JoinRule::Knock | JoinRule::Private | JoinRule::Public => {
124                Cow::Owned(JsonObject::new())
125            }
126            JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => {
127                Cow::Owned(serialize(restricted))
128            }
129            Self::_Custom(c) => Cow::Borrowed(&c.data),
130        }
131    }
132}
133
134impl<'de> Deserialize<'de> for JoinRule {
135    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136    where
137        D: de::Deserializer<'de>,
138    {
139        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
140
141        #[derive(Deserialize)]
142        struct ExtractType<'a> {
143            #[serde(borrow)]
144            join_rule: Cow<'a, str>,
145        }
146
147        let ExtractType { join_rule } = from_raw_json_value(&json)?;
148
149        match join_rule.as_ref() {
150            "invite" => Ok(Self::Invite),
151            "knock" => Ok(Self::Knock),
152            "private" => Ok(Self::Private),
153            "restricted" => from_raw_json_value(&json).map(Self::Restricted),
154            "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
155            "public" => Ok(Self::Public),
156            _ => {
157                let mut data = from_raw_json_value::<JsonObject, _>(&json)?;
158                let join_rule = as_variant!(
159                    data.remove("join_rule")
160                        .expect("we already checked that the join_rule field is present"),
161                    JsonValue::String
162                )
163                .expect("we already checked that the join rule is a string");
164
165                Ok(Self::_Custom(CustomJoinRule { join_rule, data }))
166            }
167        }
168    }
169}
170
171/// The payload for an unsupported join rule.
172#[doc(hidden)]
173#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
174pub struct CustomJoinRule {
175    /// The kind of join rule.
176    join_rule: String,
177
178    /// The remaining data.
179    #[serde(flatten)]
180    data: JsonObject,
181}
182
183/// Configuration of the `Restricted` join rule.
184#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
185#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
186pub struct Restricted {
187    /// Allow rules which describe conditions that allow joining a room.
188    #[serde(default, deserialize_with = "crate::serde::ignore_invalid_vec_items")]
189    pub allow: Vec<AllowRule>,
190}
191
192impl Restricted {
193    /// Constructs a new rule set for restricted rooms with the given rules.
194    pub fn new(allow: Vec<AllowRule>) -> Self {
195        Self { allow }
196    }
197}
198
199/// An allow rule which defines a condition that allows joining a room.
200#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
201#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
202#[serde(untagged)]
203pub enum AllowRule {
204    /// Joining is allowed if a user is already a member of the room with the id `room_id`.
205    RoomMembership(RoomMembership),
206
207    #[doc(hidden)]
208    _Custom(CustomAllowRule),
209}
210
211impl AllowRule {
212    /// Constructs an `AllowRule` with membership of the room with the given id as its predicate.
213    pub fn room_membership(room_id: OwnedRoomId) -> Self {
214        Self::RoomMembership(RoomMembership::new(room_id))
215    }
216
217    /// Returns the string name of this `AllowRule`.
218    pub fn rule_type(&self) -> &str {
219        match self {
220            AllowRule::RoomMembership(_) => "m.room_membership",
221            AllowRule::_Custom(CustomAllowRule { rule_type, .. }) => rule_type,
222        }
223    }
224
225    /// Returns the associated data of this `AllowRule`.
226    ///
227    /// The returned JSON object won't contain the `type` field, use
228    /// [`Self::rule_type`] to access that.
229    ///
230    /// Prefer to use the public variants of `AllowRule` where possible; this method is meant to
231    /// be used for custom allow rules only.
232    pub fn data(&self) -> Cow<'_, JsonObject> {
233        fn serialize<T: Serialize>(obj: &T) -> JsonObject {
234            match serde_json::to_value(obj).expect("join rule serialization should succeed") {
235                JsonValue::Object(obj) => obj,
236                _ => panic!("all message types should serialize to objects"),
237            }
238        }
239
240        match self {
241            AllowRule::RoomMembership(membership) => Cow::Owned(serialize(membership)),
242            Self::_Custom(custom) => Cow::Borrowed(&custom.data),
243        }
244    }
245}
246
247/// Allow rule which grants permission to join based on the membership of another room.
248#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
249#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
250#[serde(tag = "type", rename = "m.room_membership")]
251pub struct RoomMembership {
252    /// The id of the room which being a member of grants permission to join another room.
253    pub room_id: OwnedRoomId,
254}
255
256impl RoomMembership {
257    /// Constructs a new room membership rule for the given room id.
258    pub fn new(room_id: OwnedRoomId) -> Self {
259        Self { room_id }
260    }
261}
262
263#[doc(hidden)]
264#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
265#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
266pub struct CustomAllowRule {
267    /// The type of the allow rule.
268    #[serde(rename = "type")]
269    rule_type: String,
270
271    /// The remaining data.
272    #[serde(flatten)]
273    data: JsonObject,
274}
275
276impl<'de> Deserialize<'de> for AllowRule {
277    fn deserialize<D>(deserializer: D) -> Result<AllowRule, D::Error>
278    where
279        D: de::Deserializer<'de>,
280    {
281        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
282
283        // Extracts the `type` value.
284        #[derive(Deserialize)]
285        struct ExtractType<'a> {
286            #[serde(borrow, rename = "type")]
287            rule_type: Cow<'a, str>,
288        }
289
290        // Get the value of `type` if present.
291        let ExtractType { rule_type } = from_raw_json_value(&json)?;
292
293        match rule_type.as_ref() {
294            "m.room_membership" => from_raw_json_value(&json).map(Self::RoomMembership),
295            _ => {
296                let mut data = from_raw_json_value::<JsonObject, _>(&json)?;
297                let rule_type = as_variant!(
298                    data.remove("type").expect("we already checked that the type field is present"),
299                    JsonValue::String
300                )
301                .expect("we already checked that the type is a string");
302
303                Ok(Self::_Custom(CustomAllowRule { rule_type, data }))
304            }
305        }
306    }
307}
308
309/// The kind of rule used for users wishing to join this room.
310#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
311#[derive(Clone, Default, StringEnum)]
312#[ruma_enum(rename_all = "snake_case")]
313#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
314pub enum JoinRuleKind {
315    /// A user who wishes to join the room must first receive an invite to the room from someone
316    /// already inside of the room.
317    Invite,
318
319    /// Users can join the room if they are invited, or they can request an invite to the room.
320    ///
321    /// They can be allowed (invited) or denied (kicked/banned) access.
322    Knock,
323
324    /// Reserved but not yet implemented by the Matrix specification.
325    Private,
326
327    /// Users can join the room if they are invited, or if they meet any of the conditions
328    /// described in a set of rules.
329    Restricted,
330
331    /// Users can join the room if they are invited, or if they meet any of the conditions
332    /// described in a set of rules, or they can request an invite to the room.
333    KnockRestricted,
334
335    /// Anyone can join the room without any prior action.
336    #[default]
337    Public,
338
339    #[doc(hidden)]
340    _Custom(PrivOwnedStr),
341}
342
343impl From<JoinRuleKind> for JoinRuleSummary {
344    fn from(value: JoinRuleKind) -> Self {
345        match value {
346            JoinRuleKind::Invite => Self::Invite,
347            JoinRuleKind::Knock => Self::Knock,
348            JoinRuleKind::Private => Self::Private,
349            JoinRuleKind::Restricted => Self::Restricted(Default::default()),
350            JoinRuleKind::KnockRestricted => Self::KnockRestricted(Default::default()),
351            JoinRuleKind::Public => Self::Public,
352            JoinRuleKind::_Custom(s) => Self::_Custom(s),
353        }
354    }
355}
356
357/// The summary of a room's state.
358#[derive(Debug, Clone, Serialize)]
359#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
360pub struct RoomSummary {
361    /// The ID of the room.
362    pub room_id: OwnedRoomId,
363
364    /// The canonical alias of the room, if any.
365    ///
366    /// If the `compat-empty-string-null` cargo feature is enabled, this field being an empty
367    /// string in JSON will result in `None` here during deserialization.
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub canonical_alias: Option<OwnedRoomAliasId>,
370
371    /// The name of the room, if any.
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub name: Option<String>,
374
375    /// The topic of the room, if any.
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub topic: Option<String>,
378
379    /// The URL for the room's avatar, if one is set.
380    ///
381    /// If you activate the `compat-empty-string-null` feature, this field being an empty string in
382    /// JSON will result in `None` here during deserialization.
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub avatar_url: Option<OwnedMxcUri>,
385
386    /// The type of room from `m.room.create`, if any.
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub room_type: Option<RoomType>,
389
390    /// The number of members joined to the room.
391    pub num_joined_members: UInt,
392
393    /// The join rule of the room.
394    #[serde(flatten, skip_serializing_if = "ruma_common::serde::is_default")]
395    pub join_rule: JoinRuleSummary,
396
397    /// Whether the room may be viewed by users without joining.
398    pub world_readable: bool,
399
400    /// Whether guest users may join the room and participate in it.
401    ///
402    /// If they can, they will be subject to ordinary power level rules like any other user.
403    pub guest_can_join: bool,
404
405    /// If the room is encrypted, the algorithm used for this room.
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub encryption: Option<EventEncryptionAlgorithm>,
408
409    /// The version of the room.
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub room_version: Option<RoomVersionId>,
412}
413
414impl RoomSummary {
415    /// Construct a new `RoomSummary` with the given required fields.
416    pub fn new(
417        room_id: OwnedRoomId,
418        join_rule: JoinRuleSummary,
419        guest_can_join: bool,
420        num_joined_members: UInt,
421        world_readable: bool,
422    ) -> Self {
423        Self {
424            room_id,
425            canonical_alias: None,
426            name: None,
427            topic: None,
428            avatar_url: None,
429            room_type: None,
430            num_joined_members,
431            join_rule,
432            world_readable,
433            guest_can_join,
434            encryption: None,
435            room_version: None,
436        }
437    }
438}
439
440impl<'de> Deserialize<'de> for RoomSummary {
441    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
442    where
443        D: de::Deserializer<'de>,
444    {
445        /// Helper type to deserialize [`RoomSummary`] because using `flatten` on `join_rule`
446        /// returns an error.
447        #[derive(Deserialize)]
448        struct RoomSummaryDeHelper {
449            room_id: OwnedRoomId,
450            #[cfg_attr(
451                feature = "compat-empty-string-null",
452                serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
453            )]
454            canonical_alias: Option<OwnedRoomAliasId>,
455            name: Option<String>,
456            topic: Option<String>,
457            #[cfg_attr(
458                feature = "compat-empty-string-null",
459                serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
460            )]
461            avatar_url: Option<OwnedMxcUri>,
462            room_type: Option<RoomType>,
463            num_joined_members: UInt,
464            world_readable: bool,
465            guest_can_join: bool,
466            encryption: Option<EventEncryptionAlgorithm>,
467            room_version: Option<RoomVersionId>,
468        }
469
470        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
471        let RoomSummaryDeHelper {
472            room_id,
473            canonical_alias,
474            name,
475            topic,
476            avatar_url,
477            room_type,
478            num_joined_members,
479            world_readable,
480            guest_can_join,
481            encryption,
482            room_version,
483        } = from_raw_json_value(&json)?;
484        let join_rule: JoinRuleSummary = from_raw_json_value(&json)?;
485
486        Ok(Self {
487            room_id,
488            canonical_alias,
489            name,
490            topic,
491            avatar_url,
492            room_type,
493            num_joined_members,
494            join_rule,
495            world_readable,
496            guest_can_join,
497            encryption,
498            room_version,
499        })
500    }
501}
502
503/// The rule used for users wishing to join a room.
504///
505/// In contrast to the regular [`JoinRule`], this enum holds only simplified conditions for joining
506/// restricted rooms.
507///
508/// This type can hold an arbitrary join rule. To check for values that are not available as a
509/// documented variant here, get its kind with `.kind()` or use its string representation, obtained
510/// through `.as_str()`.
511///
512/// Because this type contains a few neighbouring fields instead of a whole object, and it is not
513/// possible to know which fields to parse for unknown variants, this type will fail to serialize if
514/// it doesn't match one of the documented variants. It is only possible to construct an
515/// undocumented variant by deserializing it, so do not re-serialize this type.
516#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
517#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
518#[serde(tag = "join_rule", rename_all = "snake_case")]
519pub enum JoinRuleSummary {
520    /// A user who wishes to join the room must first receive an invite to the room from someone
521    /// already inside of the room.
522    Invite,
523
524    /// Users can join the room if they are invited, or they can request an invite to the room.
525    ///
526    /// They can be allowed (invited) or denied (kicked/banned) access.
527    Knock,
528
529    /// Reserved but not yet implemented by the Matrix specification.
530    Private,
531
532    /// Users can join the room if they are invited, or if they meet one of the conditions
533    /// described in the [`RestrictedSummary`].
534    Restricted(RestrictedSummary),
535
536    /// Users can join the room if they are invited, or if they meet one of the conditions
537    /// described in the [`RestrictedSummary`], or they can request an invite to the room.
538    KnockRestricted(RestrictedSummary),
539
540    /// Anyone can join the room without any prior action.
541    #[default]
542    Public,
543
544    #[doc(hidden)]
545    #[serde(skip_serializing)]
546    _Custom(PrivOwnedStr),
547}
548
549impl JoinRuleSummary {
550    /// Returns the kind of this `JoinRuleSummary`.
551    pub fn kind(&self) -> JoinRuleKind {
552        match self {
553            Self::Invite => JoinRuleKind::Invite,
554            Self::Knock => JoinRuleKind::Knock,
555            Self::Private => JoinRuleKind::Private,
556            Self::Restricted(_) => JoinRuleKind::Restricted,
557            Self::KnockRestricted(_) => JoinRuleKind::KnockRestricted,
558            Self::Public => JoinRuleKind::Public,
559            Self::_Custom(rule) => JoinRuleKind::_Custom(rule.clone()),
560        }
561    }
562
563    /// Returns the string name of this `JoinRuleSummary`.
564    pub fn as_str(&self) -> &str {
565        match self {
566            Self::Invite => "invite",
567            Self::Knock => "knock",
568            Self::Private => "private",
569            Self::Restricted(_) => "restricted",
570            Self::KnockRestricted(_) => "knock_restricted",
571            Self::Public => "public",
572            Self::_Custom(rule) => &rule.0,
573        }
574    }
575}
576
577impl From<JoinRule> for JoinRuleSummary {
578    fn from(value: JoinRule) -> Self {
579        match value {
580            JoinRule::Invite => Self::Invite,
581            JoinRule::Knock => Self::Knock,
582            JoinRule::Private => Self::Private,
583            JoinRule::Restricted(restricted) => Self::Restricted(restricted.into()),
584            JoinRule::KnockRestricted(restricted) => Self::KnockRestricted(restricted.into()),
585            JoinRule::Public => Self::Public,
586            JoinRule::_Custom(CustomJoinRule { join_rule, .. }) => {
587                Self::_Custom(PrivOwnedStr(join_rule.into()))
588            }
589        }
590    }
591}
592
593impl<'de> Deserialize<'de> for JoinRuleSummary {
594    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
595    where
596        D: de::Deserializer<'de>,
597    {
598        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
599
600        #[derive(Deserialize)]
601        struct ExtractType<'a> {
602            #[serde(borrow)]
603            join_rule: Option<Cow<'a, str>>,
604        }
605
606        let Some(join_rule) = serde_json::from_str::<ExtractType<'_>>(json.get())
607            .map_err(de::Error::custom)?
608            .join_rule
609        else {
610            return Ok(Self::default());
611        };
612
613        match join_rule.as_ref() {
614            "invite" => Ok(Self::Invite),
615            "knock" => Ok(Self::Knock),
616            "private" => Ok(Self::Private),
617            "restricted" => from_raw_json_value(&json).map(Self::Restricted),
618            "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
619            "public" => Ok(Self::Public),
620            _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
621        }
622    }
623}
624
625/// A summary of the conditions for joining a restricted room.
626#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
627#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
628pub struct RestrictedSummary {
629    /// The room IDs which are specified by the join rules.
630    #[serde(default)]
631    pub allowed_room_ids: Vec<OwnedRoomId>,
632}
633
634impl RestrictedSummary {
635    /// Constructs a new `RestrictedSummary` with the given room IDs.
636    pub fn new(allowed_room_ids: Vec<OwnedRoomId>) -> Self {
637        Self { allowed_room_ids }
638    }
639}
640
641impl From<Restricted> for RestrictedSummary {
642    fn from(value: Restricted) -> Self {
643        let allowed_room_ids = value
644            .allow
645            .into_iter()
646            .filter_map(|allow_rule| {
647                let membership = as_variant!(allow_rule, AllowRule::RoomMembership)?;
648                Some(membership.room_id)
649            })
650            .collect();
651
652        Self::new(allowed_room_ids)
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use assert_matches2::{assert_let, assert_matches};
659    use js_int::uint;
660    use ruma_common::{OwnedRoomId, owned_room_id};
661    use serde_json::{Value as JsonValue, from_value as from_json_value, json};
662
663    use super::{
664        AllowRule, CustomAllowRule, JoinRule, JoinRuleSummary, Restricted, RestrictedSummary,
665        RoomMembership, RoomSummary,
666    };
667    use crate::{assert_to_canonical_json_eq, serde::JsonObject};
668
669    #[test]
670    fn deserialize_summary_no_join_rule() {
671        let json = json!({
672            "room_id": "!room:localhost",
673            "num_joined_members": 5,
674            "world_readable": false,
675            "guest_can_join": false,
676        });
677
678        let summary: RoomSummary = from_json_value(json).unwrap();
679        assert_eq!(summary.room_id, "!room:localhost");
680        assert_eq!(summary.num_joined_members, uint!(5));
681        assert!(!summary.world_readable);
682        assert!(!summary.guest_can_join);
683        assert_matches!(summary.join_rule, JoinRuleSummary::Public);
684    }
685
686    #[test]
687    fn deserialize_summary_private_join_rule() {
688        let json = json!({
689            "room_id": "!room:localhost",
690            "num_joined_members": 5,
691            "world_readable": false,
692            "guest_can_join": false,
693            "join_rule": "private",
694        });
695
696        let summary: RoomSummary = from_json_value(json).unwrap();
697        assert_eq!(summary.room_id, "!room:localhost");
698        assert_eq!(summary.num_joined_members, uint!(5));
699        assert!(!summary.world_readable);
700        assert!(!summary.guest_can_join);
701        assert_matches!(summary.join_rule, JoinRuleSummary::Private);
702    }
703
704    #[test]
705    fn deserialize_summary_restricted_join_rule() {
706        let json = json!({
707            "room_id": "!room:localhost",
708            "num_joined_members": 5,
709            "world_readable": false,
710            "guest_can_join": false,
711            "join_rule": "restricted",
712            "allowed_room_ids": ["!otherroom:localhost"],
713        });
714
715        let summary: RoomSummary = from_json_value(json).unwrap();
716        assert_eq!(summary.room_id, "!room:localhost");
717        assert_eq!(summary.num_joined_members, uint!(5));
718        assert!(!summary.world_readable);
719        assert!(!summary.guest_can_join);
720        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
721        assert_eq!(restricted.allowed_room_ids.len(), 1);
722    }
723
724    #[test]
725    fn deserialize_summary_restricted_join_rule_no_allowed_room_ids() {
726        let json = json!({
727            "room_id": "!room:localhost",
728            "num_joined_members": 5,
729            "world_readable": false,
730            "guest_can_join": false,
731            "join_rule": "restricted",
732        });
733
734        let summary: RoomSummary = from_json_value(json).unwrap();
735        assert_eq!(summary.room_id, "!room:localhost");
736        assert_eq!(summary.num_joined_members, uint!(5));
737        assert!(!summary.world_readable);
738        assert!(!summary.guest_can_join);
739        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
740        assert_eq!(restricted.allowed_room_ids.len(), 0);
741    }
742
743    #[test]
744    fn serialize_summary_knock_join_rule() {
745        let summary = RoomSummary::new(
746            owned_room_id!("!room:localhost"),
747            JoinRuleSummary::Knock,
748            false,
749            uint!(5),
750            false,
751        );
752
753        assert_to_canonical_json_eq!(
754            summary,
755            json!({
756                "room_id": "!room:localhost",
757                "num_joined_members": 5,
758                "world_readable": false,
759                "guest_can_join": false,
760                "join_rule": "knock",
761            })
762        );
763    }
764
765    #[test]
766    fn serialize_summary_restricted_join_rule() {
767        let summary = RoomSummary::new(
768            owned_room_id!("!room:localhost"),
769            JoinRuleSummary::Restricted(RestrictedSummary::new(vec![owned_room_id!(
770                "!otherroom:localhost"
771            )])),
772            false,
773            uint!(5),
774            false,
775        );
776
777        assert_to_canonical_json_eq!(
778            summary,
779            json!({
780                "room_id": "!room:localhost",
781                "num_joined_members": 5,
782                "world_readable": false,
783                "guest_can_join": false,
784                "join_rule": "restricted",
785                "allowed_room_ids": ["!otherroom:localhost"],
786            })
787        );
788    }
789
790    #[test]
791    fn custom_join_rule_serialize_roundtrip() {
792        let json = json!({
793            "join_rule": "local.dev.unicorns",
794            "rainbows": true,
795        });
796
797        let join_rule = from_json_value::<JoinRule>(json.clone()).unwrap();
798        assert_eq!(join_rule.kind().as_str(), "local.dev.unicorns");
799        let data = &*join_rule.data();
800        assert_eq!(data.len(), 1);
801        assert_let!(Some(JsonValue::Bool(value)) = data.get("rainbows"));
802        assert!(value);
803
804        assert_to_canonical_json_eq!(join_rule, json);
805    }
806
807    #[test]
808    fn join_rule_to_join_rule_summary() {
809        assert_eq!(JoinRuleSummary::Invite, JoinRule::Invite.into());
810        assert_eq!(JoinRuleSummary::Knock, JoinRule::Knock.into());
811        assert_eq!(JoinRuleSummary::Public, JoinRule::Public.into());
812        assert_eq!(JoinRuleSummary::Private, JoinRule::Private.into());
813
814        assert_matches!(
815            JoinRule::KnockRestricted(Restricted::default()).into(),
816            JoinRuleSummary::KnockRestricted(restricted)
817        );
818        assert_eq!(restricted.allowed_room_ids, &[] as &[OwnedRoomId]);
819
820        let room_id = owned_room_id!("!room:localhost");
821        assert_matches!(
822            JoinRule::Restricted(Restricted::new(vec![AllowRule::RoomMembership(
823                RoomMembership::new(room_id.clone())
824            )]))
825            .into(),
826            JoinRuleSummary::Restricted(restricted)
827        );
828        assert_eq!(restricted.allowed_room_ids, [room_id]);
829    }
830
831    #[test]
832    fn roundtrip_custom_allow_rule() {
833        let json = json!({ "type": "org.msc9000.something", "foo": "bar"});
834
835        let allow_rule: AllowRule = from_json_value(json.clone()).unwrap();
836        assert_eq!(allow_rule.rule_type(), "org.msc9000.something");
837        let data = &*allow_rule.data();
838        assert_eq!(data.len(), 1);
839        assert_let!(Some(JsonValue::String(value)) = data.get("foo"));
840        assert_eq!(value, "bar");
841
842        assert_to_canonical_json_eq!(allow_rule, json);
843    }
844
845    #[test]
846    fn invalid_allow_items() {
847        let json = r#"{
848            "join_rule": "restricted",
849            "allow": [
850                {
851                    "type": "m.room_membership",
852                    "room_id": "!mods:example.org"
853                },
854                {
855                    "type": "m.room_membership",
856                    "room_id": ""
857                },
858                {
859                    "type": "m.room_membership",
860                    "room_id": "not a room id"
861                },
862                {
863                    "type": "org.example.custom",
864                    "org.example.minimum_role": "developer"
865                },
866                {
867                    "not even close": "to being correct",
868                    "any object": "passes this test",
869                    "only non-objects in this array": "cause deserialization to fail"
870                }
871            ]
872        }"#;
873        let join_rule: JoinRule = serde_json::from_str(json).unwrap();
874
875        assert_matches!(join_rule, JoinRule::Restricted(restricted));
876        assert_eq!(
877            restricted.allow,
878            &[
879                AllowRule::room_membership(owned_room_id!("!mods:example.org")),
880                AllowRule::_Custom(CustomAllowRule {
881                    rule_type: "org.example.custom".into(),
882                    data: JsonObject::from_iter([(
883                        "org.example.minimum_role".to_owned(),
884                        "developer".into()
885                    )]),
886                })
887            ]
888        );
889    }
890}