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, to_value as to_json_value};
610
611    use super::{
612        AllowRule, CustomAllowRule, JoinRule, JoinRuleSummary, Restricted, RestrictedSummary,
613        RoomMembership, RoomSummary,
614    };
615
616    #[test]
617    fn deserialize_summary_no_join_rule() {
618        let json = json!({
619            "room_id": "!room:localhost",
620            "num_joined_members": 5,
621            "world_readable": false,
622            "guest_can_join": false,
623        });
624
625        let summary: RoomSummary = from_json_value(json).unwrap();
626        assert_eq!(summary.room_id, "!room:localhost");
627        assert_eq!(summary.num_joined_members, uint!(5));
628        assert!(!summary.world_readable);
629        assert!(!summary.guest_can_join);
630        assert_matches!(summary.join_rule, JoinRuleSummary::Public);
631    }
632
633    #[test]
634    fn deserialize_summary_private_join_rule() {
635        let json = json!({
636            "room_id": "!room:localhost",
637            "num_joined_members": 5,
638            "world_readable": false,
639            "guest_can_join": false,
640            "join_rule": "private",
641        });
642
643        let summary: RoomSummary = from_json_value(json).unwrap();
644        assert_eq!(summary.room_id, "!room:localhost");
645        assert_eq!(summary.num_joined_members, uint!(5));
646        assert!(!summary.world_readable);
647        assert!(!summary.guest_can_join);
648        assert_matches!(summary.join_rule, JoinRuleSummary::Private);
649    }
650
651    #[test]
652    fn deserialize_summary_restricted_join_rule() {
653        let json = json!({
654            "room_id": "!room:localhost",
655            "num_joined_members": 5,
656            "world_readable": false,
657            "guest_can_join": false,
658            "join_rule": "restricted",
659            "allowed_room_ids": ["!otherroom:localhost"],
660        });
661
662        let summary: RoomSummary = from_json_value(json).unwrap();
663        assert_eq!(summary.room_id, "!room:localhost");
664        assert_eq!(summary.num_joined_members, uint!(5));
665        assert!(!summary.world_readable);
666        assert!(!summary.guest_can_join);
667        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
668        assert_eq!(restricted.allowed_room_ids.len(), 1);
669    }
670
671    #[test]
672    fn deserialize_summary_restricted_join_rule_no_allowed_room_ids() {
673        let json = json!({
674            "room_id": "!room:localhost",
675            "num_joined_members": 5,
676            "world_readable": false,
677            "guest_can_join": false,
678            "join_rule": "restricted",
679        });
680
681        let summary: RoomSummary = from_json_value(json).unwrap();
682        assert_eq!(summary.room_id, "!room:localhost");
683        assert_eq!(summary.num_joined_members, uint!(5));
684        assert!(!summary.world_readable);
685        assert!(!summary.guest_can_join);
686        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
687        assert_eq!(restricted.allowed_room_ids.len(), 0);
688    }
689
690    #[test]
691    fn serialize_summary_knock_join_rule() {
692        let summary = RoomSummary::new(
693            owned_room_id!("!room:localhost"),
694            JoinRuleSummary::Knock,
695            false,
696            uint!(5),
697            false,
698        );
699
700        assert_eq!(
701            to_json_value(&summary).unwrap(),
702            json!({
703                "room_id": "!room:localhost",
704                "num_joined_members": 5,
705                "world_readable": false,
706                "guest_can_join": false,
707                "join_rule": "knock",
708            })
709        );
710    }
711
712    #[test]
713    fn serialize_summary_restricted_join_rule() {
714        let summary = RoomSummary::new(
715            owned_room_id!("!room:localhost"),
716            JoinRuleSummary::Restricted(RestrictedSummary::new(vec![owned_room_id!(
717                "!otherroom:localhost"
718            )])),
719            false,
720            uint!(5),
721            false,
722        );
723
724        assert_eq!(
725            to_json_value(&summary).unwrap(),
726            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                "allowed_room_ids": ["!otherroom:localhost"],
733            })
734        );
735    }
736
737    #[test]
738    fn join_rule_to_join_rule_summary() {
739        assert_eq!(JoinRuleSummary::Invite, JoinRule::Invite.into());
740        assert_eq!(JoinRuleSummary::Knock, JoinRule::Knock.into());
741        assert_eq!(JoinRuleSummary::Public, JoinRule::Public.into());
742        assert_eq!(JoinRuleSummary::Private, JoinRule::Private.into());
743
744        assert_matches!(
745            JoinRule::KnockRestricted(Restricted::default()).into(),
746            JoinRuleSummary::KnockRestricted(restricted)
747        );
748        assert_eq!(restricted.allowed_room_ids, &[] as &[OwnedRoomId]);
749
750        let room_id = owned_room_id!("!room:localhost");
751        assert_matches!(
752            JoinRule::Restricted(Restricted::new(vec![AllowRule::RoomMembership(
753                RoomMembership::new(room_id.clone())
754            )]))
755            .into(),
756            JoinRuleSummary::Restricted(restricted)
757        );
758        assert_eq!(restricted.allowed_room_ids, [room_id]);
759    }
760
761    #[test]
762    fn roundtrip_custom_allow_rule() {
763        let json = r#"{"type":"org.msc9000.something","foo":"bar"}"#;
764        let allow_rule: AllowRule = serde_json::from_str(json).unwrap();
765        assert_matches!(&allow_rule, AllowRule::_Custom(_));
766        assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json);
767    }
768
769    #[test]
770    fn invalid_allow_items() {
771        let json = r#"{
772            "join_rule": "restricted",
773            "allow": [
774                {
775                    "type": "m.room_membership",
776                    "room_id": "!mods:example.org"
777                },
778                {
779                    "type": "m.room_membership",
780                    "room_id": ""
781                },
782                {
783                    "type": "m.room_membership",
784                    "room_id": "not a room id"
785                },
786                {
787                    "type": "org.example.custom",
788                    "org.example.minimum_role": "developer"
789                },
790                {
791                    "not even close": "to being correct",
792                    "any object": "passes this test",
793                    "only non-objects in this array": "cause deserialization to fail"
794                }
795            ]
796        }"#;
797        let join_rule: JoinRule = serde_json::from_str(json).unwrap();
798
799        assert_matches!(join_rule, JoinRule::Restricted(restricted));
800        assert_eq!(
801            restricted.allow,
802            &[
803                AllowRule::room_membership(owned_room_id!("!mods:example.org")),
804                AllowRule::_Custom(Box::new(CustomAllowRule {
805                    rule_type: "org.example.custom".into(),
806                    extra: BTreeMap::from([(
807                        "org.example.minimum_role".into(),
808                        "developer".into()
809                    )])
810                }))
811            ]
812        );
813    }
814}