ruma_common/
room.rs

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