1use std::{borrow::Cow, collections::BTreeMap};
4
5use as_variant::as_variant;
6use js_int::UInt;
7use serde::{de, Deserialize, Serialize};
8use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue};
9
10use crate::{
11    serde::{from_raw_json_value, StringEnum},
12    EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr,
13    RoomVersionId,
14};
15
16#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
18#[derive(Clone, PartialEq, Eq, StringEnum)]
19#[non_exhaustive]
20pub enum RoomType {
21    #[ruma_enum(rename = "m.space")]
23    Space,
24
25    #[doc(hidden)]
27    _Custom(PrivOwnedStr),
28}
29
30#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
35#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
36#[serde(tag = "join_rule", rename_all = "snake_case")]
37pub enum JoinRule {
38    Invite,
41
42    Knock,
46
47    Private,
49
50    Restricted(Restricted),
53
54    KnockRestricted(Restricted),
57
58    Public,
60
61    #[doc(hidden)]
62    #[serde(skip_serializing)]
63    _Custom(PrivOwnedStr),
64}
65
66impl JoinRule {
67    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(rule) => JoinRuleKind::_Custom(rule.clone()),
77        }
78    }
79
80    pub fn as_str(&self) -> &str {
82        match self {
83            JoinRule::Invite => "invite",
84            JoinRule::Knock => "knock",
85            JoinRule::Private => "private",
86            JoinRule::Restricted(_) => "restricted",
87            JoinRule::KnockRestricted(_) => "knock_restricted",
88            JoinRule::Public => "public",
89            JoinRule::_Custom(rule) => &rule.0,
90        }
91    }
92}
93
94impl<'de> Deserialize<'de> for JoinRule {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: de::Deserializer<'de>,
98    {
99        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
100
101        #[derive(Deserialize)]
102        struct ExtractType<'a> {
103            #[serde(borrow)]
104            join_rule: Option<Cow<'a, str>>,
105        }
106
107        let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get())
108            .map_err(de::Error::custom)?
109            .join_rule
110            .ok_or_else(|| de::Error::missing_field("join_rule"))?;
111
112        match join_rule.as_ref() {
113            "invite" => Ok(Self::Invite),
114            "knock" => Ok(Self::Knock),
115            "private" => Ok(Self::Private),
116            "restricted" => from_raw_json_value(&json).map(Self::Restricted),
117            "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
118            "public" => Ok(Self::Public),
119            _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
120        }
121    }
122}
123
124#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
126#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
127pub struct Restricted {
128    #[serde(default, deserialize_with = "crate::serde::ignore_invalid_vec_items")]
130    pub allow: Vec<AllowRule>,
131}
132
133impl Restricted {
134    pub fn new(allow: Vec<AllowRule>) -> Self {
136        Self { allow }
137    }
138}
139
140#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
142#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
143#[serde(untagged)]
144pub enum AllowRule {
145    RoomMembership(RoomMembership),
147
148    #[doc(hidden)]
149    _Custom(Box<CustomAllowRule>),
150}
151
152impl AllowRule {
153    pub fn room_membership(room_id: OwnedRoomId) -> Self {
155        Self::RoomMembership(RoomMembership::new(room_id))
156    }
157}
158
159#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
161#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
162#[serde(tag = "type", rename = "m.room_membership")]
163pub struct RoomMembership {
164    pub room_id: OwnedRoomId,
166}
167
168impl RoomMembership {
169    pub fn new(room_id: OwnedRoomId) -> Self {
171        Self { room_id }
172    }
173}
174
175#[doc(hidden)]
176#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
177#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
178pub struct CustomAllowRule {
179    #[serde(rename = "type")]
180    rule_type: String,
181    #[serde(flatten)]
182    extra: BTreeMap<String, JsonValue>,
183}
184
185impl<'de> Deserialize<'de> for AllowRule {
186    fn deserialize<D>(deserializer: D) -> Result<AllowRule, D::Error>
187    where
188        D: de::Deserializer<'de>,
189    {
190        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
191
192        #[derive(Deserialize)]
194        struct ExtractType<'a> {
195            #[serde(borrow, rename = "type")]
196            rule_type: Option<Cow<'a, str>>,
197        }
198
199        let rule_type = serde_json::from_str::<ExtractType<'_>>(json.get())
201            .map_err(de::Error::custom)?
202            .rule_type;
203
204        match rule_type.as_deref() {
205            Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership),
206            Some(_) => from_raw_json_value(&json).map(Self::_Custom),
207            None => Err(de::Error::missing_field("type")),
208        }
209    }
210}
211
212#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
214#[derive(Clone, Default, PartialEq, Eq, StringEnum)]
215#[ruma_enum(rename_all = "snake_case")]
216#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
217pub enum JoinRuleKind {
218    Invite,
221
222    Knock,
226
227    Private,
229
230    Restricted,
233
234    KnockRestricted,
237
238    #[default]
240    Public,
241
242    #[doc(hidden)]
243    _Custom(PrivOwnedStr),
244}
245
246impl From<JoinRuleKind> for JoinRuleSummary {
247    fn from(value: JoinRuleKind) -> Self {
248        match value {
249            JoinRuleKind::Invite => Self::Invite,
250            JoinRuleKind::Knock => Self::Knock,
251            JoinRuleKind::Private => Self::Private,
252            JoinRuleKind::Restricted => Self::Restricted(Default::default()),
253            JoinRuleKind::KnockRestricted => Self::KnockRestricted(Default::default()),
254            JoinRuleKind::Public => Self::Public,
255            JoinRuleKind::_Custom(s) => Self::_Custom(s),
256        }
257    }
258}
259
260#[derive(Debug, Clone, Serialize)]
262#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
263pub struct RoomSummary {
264    pub room_id: OwnedRoomId,
266
267    #[serde(skip_serializing_if = "Option::is_none")]
272    pub canonical_alias: Option<OwnedRoomAliasId>,
273
274    #[serde(skip_serializing_if = "Option::is_none")]
276    pub name: Option<String>,
277
278    #[serde(skip_serializing_if = "Option::is_none")]
280    pub topic: Option<String>,
281
282    #[serde(skip_serializing_if = "Option::is_none")]
287    pub avatar_url: Option<OwnedMxcUri>,
288
289    #[serde(skip_serializing_if = "Option::is_none")]
291    pub room_type: Option<RoomType>,
292
293    pub num_joined_members: UInt,
295
296    #[serde(flatten, skip_serializing_if = "ruma_common::serde::is_default")]
298    pub join_rule: JoinRuleSummary,
299
300    pub world_readable: bool,
302
303    pub guest_can_join: bool,
307
308    #[serde(skip_serializing_if = "Option::is_none")]
310    pub encryption: Option<EventEncryptionAlgorithm>,
311
312    #[serde(skip_serializing_if = "Option::is_none")]
314    pub room_version: Option<RoomVersionId>,
315}
316
317impl RoomSummary {
318    pub fn new(
320        room_id: OwnedRoomId,
321        join_rule: JoinRuleSummary,
322        guest_can_join: bool,
323        num_joined_members: UInt,
324        world_readable: bool,
325    ) -> Self {
326        Self {
327            room_id,
328            canonical_alias: None,
329            name: None,
330            topic: None,
331            avatar_url: None,
332            room_type: None,
333            num_joined_members,
334            join_rule,
335            world_readable,
336            guest_can_join,
337            encryption: None,
338            room_version: None,
339        }
340    }
341}
342
343impl<'de> Deserialize<'de> for RoomSummary {
344    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
345    where
346        D: de::Deserializer<'de>,
347    {
348        #[derive(Deserialize)]
351        struct RoomSummaryDeHelper {
352            room_id: OwnedRoomId,
353            #[cfg_attr(
354                feature = "compat-empty-string-null",
355                serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
356            )]
357            canonical_alias: Option<OwnedRoomAliasId>,
358            name: Option<String>,
359            topic: Option<String>,
360            #[cfg_attr(
361                feature = "compat-empty-string-null",
362                serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
363            )]
364            avatar_url: Option<OwnedMxcUri>,
365            room_type: Option<RoomType>,
366            num_joined_members: UInt,
367            world_readable: bool,
368            guest_can_join: bool,
369            encryption: Option<EventEncryptionAlgorithm>,
370            room_version: Option<RoomVersionId>,
371        }
372
373        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
374        let RoomSummaryDeHelper {
375            room_id,
376            canonical_alias,
377            name,
378            topic,
379            avatar_url,
380            room_type,
381            num_joined_members,
382            world_readable,
383            guest_can_join,
384            encryption,
385            room_version,
386        } = from_raw_json_value(&json)?;
387        let join_rule: JoinRuleSummary = from_raw_json_value(&json)?;
388
389        Ok(Self {
390            room_id,
391            canonical_alias,
392            name,
393            topic,
394            avatar_url,
395            room_type,
396            num_joined_members,
397            join_rule,
398            world_readable,
399            guest_can_join,
400            encryption,
401            room_version,
402        })
403    }
404}
405
406#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
411#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
412#[serde(tag = "join_rule", rename_all = "snake_case")]
413pub enum JoinRuleSummary {
414    Invite,
417
418    Knock,
422
423    Private,
425
426    Restricted(RestrictedSummary),
429
430    KnockRestricted(RestrictedSummary),
433
434    #[default]
436    Public,
437
438    #[doc(hidden)]
439    #[serde(skip_serializing)]
440    _Custom(PrivOwnedStr),
441}
442
443impl JoinRuleSummary {
444    pub fn kind(&self) -> JoinRuleKind {
446        match self {
447            Self::Invite => JoinRuleKind::Invite,
448            Self::Knock => JoinRuleKind::Knock,
449            Self::Private => JoinRuleKind::Private,
450            Self::Restricted(_) => JoinRuleKind::Restricted,
451            Self::KnockRestricted(_) => JoinRuleKind::KnockRestricted,
452            Self::Public => JoinRuleKind::Public,
453            Self::_Custom(rule) => JoinRuleKind::_Custom(rule.clone()),
454        }
455    }
456
457    pub fn as_str(&self) -> &str {
459        match self {
460            Self::Invite => "invite",
461            Self::Knock => "knock",
462            Self::Private => "private",
463            Self::Restricted(_) => "restricted",
464            Self::KnockRestricted(_) => "knock_restricted",
465            Self::Public => "public",
466            Self::_Custom(rule) => &rule.0,
467        }
468    }
469}
470
471impl From<JoinRule> for JoinRuleSummary {
472    fn from(value: JoinRule) -> Self {
473        match value {
474            JoinRule::Invite => Self::Invite,
475            JoinRule::Knock => Self::Knock,
476            JoinRule::Private => Self::Private,
477            JoinRule::Restricted(restricted) => Self::Restricted(restricted.into()),
478            JoinRule::KnockRestricted(restricted) => Self::KnockRestricted(restricted.into()),
479            JoinRule::Public => Self::Public,
480            JoinRule::_Custom(rule) => Self::_Custom(rule),
481        }
482    }
483}
484
485impl<'de> Deserialize<'de> for JoinRuleSummary {
486    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
487    where
488        D: de::Deserializer<'de>,
489    {
490        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
491
492        #[derive(Deserialize)]
493        struct ExtractType<'a> {
494            #[serde(borrow)]
495            join_rule: Option<Cow<'a, str>>,
496        }
497
498        let Some(join_rule) = serde_json::from_str::<ExtractType<'_>>(json.get())
499            .map_err(de::Error::custom)?
500            .join_rule
501        else {
502            return Ok(Self::default());
503        };
504
505        match join_rule.as_ref() {
506            "invite" => Ok(Self::Invite),
507            "knock" => Ok(Self::Knock),
508            "private" => Ok(Self::Private),
509            "restricted" => from_raw_json_value(&json).map(Self::Restricted),
510            "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
511            "public" => Ok(Self::Public),
512            _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
513        }
514    }
515}
516
517#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
519#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
520pub struct RestrictedSummary {
521    #[serde(default)]
523    pub allowed_room_ids: Vec<OwnedRoomId>,
524}
525
526impl RestrictedSummary {
527    pub fn new(allowed_room_ids: Vec<OwnedRoomId>) -> Self {
529        Self { allowed_room_ids }
530    }
531}
532
533impl From<Restricted> for RestrictedSummary {
534    fn from(value: Restricted) -> Self {
535        let allowed_room_ids = value
536            .allow
537            .into_iter()
538            .filter_map(|allow_rule| {
539                let membership = as_variant!(allow_rule, AllowRule::RoomMembership)?;
540                Some(membership.room_id)
541            })
542            .collect();
543
544        Self::new(allowed_room_ids)
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use std::collections::BTreeMap;
551
552    use assert_matches2::assert_matches;
553    use js_int::uint;
554    use ruma_common::{owned_room_id, OwnedRoomId};
555    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
556
557    use super::{
558        AllowRule, CustomAllowRule, JoinRule, JoinRuleSummary, Restricted, RestrictedSummary,
559        RoomMembership, RoomSummary,
560    };
561
562    #[test]
563    fn deserialize_summary_no_join_rule() {
564        let json = json!({
565            "room_id": "!room:localhost",
566            "num_joined_members": 5,
567            "world_readable": false,
568            "guest_can_join": false,
569        });
570
571        let summary: RoomSummary = from_json_value(json).unwrap();
572        assert_eq!(summary.room_id, "!room:localhost");
573        assert_eq!(summary.num_joined_members, uint!(5));
574        assert!(!summary.world_readable);
575        assert!(!summary.guest_can_join);
576        assert_matches!(summary.join_rule, JoinRuleSummary::Public);
577    }
578
579    #[test]
580    fn deserialize_summary_private_join_rule() {
581        let json = json!({
582            "room_id": "!room:localhost",
583            "num_joined_members": 5,
584            "world_readable": false,
585            "guest_can_join": false,
586            "join_rule": "private",
587        });
588
589        let summary: RoomSummary = from_json_value(json).unwrap();
590        assert_eq!(summary.room_id, "!room:localhost");
591        assert_eq!(summary.num_joined_members, uint!(5));
592        assert!(!summary.world_readable);
593        assert!(!summary.guest_can_join);
594        assert_matches!(summary.join_rule, JoinRuleSummary::Private);
595    }
596
597    #[test]
598    fn deserialize_summary_restricted_join_rule() {
599        let json = json!({
600            "room_id": "!room:localhost",
601            "num_joined_members": 5,
602            "world_readable": false,
603            "guest_can_join": false,
604            "join_rule": "restricted",
605            "allowed_room_ids": ["!otherroom:localhost"],
606        });
607
608        let summary: RoomSummary = from_json_value(json).unwrap();
609        assert_eq!(summary.room_id, "!room:localhost");
610        assert_eq!(summary.num_joined_members, uint!(5));
611        assert!(!summary.world_readable);
612        assert!(!summary.guest_can_join);
613        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
614        assert_eq!(restricted.allowed_room_ids.len(), 1);
615    }
616
617    #[test]
618    fn deserialize_summary_restricted_join_rule_no_allowed_room_ids() {
619        let json = json!({
620            "room_id": "!room:localhost",
621            "num_joined_members": 5,
622            "world_readable": false,
623            "guest_can_join": false,
624            "join_rule": "restricted",
625        });
626
627        let summary: RoomSummary = from_json_value(json).unwrap();
628        assert_eq!(summary.room_id, "!room:localhost");
629        assert_eq!(summary.num_joined_members, uint!(5));
630        assert!(!summary.world_readable);
631        assert!(!summary.guest_can_join);
632        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
633        assert_eq!(restricted.allowed_room_ids.len(), 0);
634    }
635
636    #[test]
637    fn serialize_summary_knock_join_rule() {
638        let summary = RoomSummary::new(
639            owned_room_id!("!room:localhost"),
640            JoinRuleSummary::Knock,
641            false,
642            uint!(5),
643            false,
644        );
645
646        assert_eq!(
647            to_json_value(&summary).unwrap(),
648            json!({
649                "room_id": "!room:localhost",
650                "num_joined_members": 5,
651                "world_readable": false,
652                "guest_can_join": false,
653                "join_rule": "knock",
654            })
655        );
656    }
657
658    #[test]
659    fn serialize_summary_restricted_join_rule() {
660        let summary = RoomSummary::new(
661            owned_room_id!("!room:localhost"),
662            JoinRuleSummary::Restricted(RestrictedSummary::new(vec![owned_room_id!(
663                "!otherroom:localhost"
664            )])),
665            false,
666            uint!(5),
667            false,
668        );
669
670        assert_eq!(
671            to_json_value(&summary).unwrap(),
672            json!({
673                "room_id": "!room:localhost",
674                "num_joined_members": 5,
675                "world_readable": false,
676                "guest_can_join": false,
677                "join_rule": "restricted",
678                "allowed_room_ids": ["!otherroom:localhost"],
679            })
680        );
681    }
682
683    #[test]
684    fn join_rule_to_join_rule_summary() {
685        assert_eq!(JoinRuleSummary::Invite, JoinRule::Invite.into());
686        assert_eq!(JoinRuleSummary::Knock, JoinRule::Knock.into());
687        assert_eq!(JoinRuleSummary::Public, JoinRule::Public.into());
688        assert_eq!(JoinRuleSummary::Private, JoinRule::Private.into());
689
690        assert_matches!(
691            JoinRule::KnockRestricted(Restricted::default()).into(),
692            JoinRuleSummary::KnockRestricted(restricted)
693        );
694        assert_eq!(restricted.allowed_room_ids, &[] as &[OwnedRoomId]);
695
696        let room_id = owned_room_id!("!room:localhost");
697        assert_matches!(
698            JoinRule::Restricted(Restricted::new(vec![AllowRule::RoomMembership(
699                RoomMembership::new(room_id.clone())
700            )]))
701            .into(),
702            JoinRuleSummary::Restricted(restricted)
703        );
704        assert_eq!(restricted.allowed_room_ids, [room_id]);
705    }
706
707    #[test]
708    fn roundtrip_custom_allow_rule() {
709        let json = r#"{"type":"org.msc9000.something","foo":"bar"}"#;
710        let allow_rule: AllowRule = serde_json::from_str(json).unwrap();
711        assert_matches!(&allow_rule, AllowRule::_Custom(_));
712        assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json);
713    }
714
715    #[test]
716    fn invalid_allow_items() {
717        let json = r#"{
718            "join_rule": "restricted",
719            "allow": [
720                {
721                    "type": "m.room_membership",
722                    "room_id": "!mods:example.org"
723                },
724                {
725                    "type": "m.room_membership",
726                    "room_id": ""
727                },
728                {
729                    "type": "m.room_membership",
730                    "room_id": "not a room id"
731                },
732                {
733                    "type": "org.example.custom",
734                    "org.example.minimum_role": "developer"
735                },
736                {
737                    "not even close": "to being correct",
738                    "any object": "passes this test",
739                    "only non-objects in this array": "cause deserialization to fail"
740                }
741            ]
742        }"#;
743        let join_rule: JoinRule = serde_json::from_str(json).unwrap();
744
745        assert_matches!(join_rule, JoinRule::Restricted(restricted));
746        assert_eq!(
747            restricted.allow,
748            &[
749                AllowRule::room_membership(owned_room_id!("!mods:example.org")),
750                AllowRule::_Custom(Box::new(CustomAllowRule {
751                    rule_type: "org.example.custom".into(),
752                    extra: BTreeMap::from([(
753                        "org.example.minimum_role".into(),
754                        "developer".into()
755                    )])
756                }))
757            ]
758        );
759    }
760}