1use 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#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
18#[derive(Clone, StringEnum)]
19#[non_exhaustive]
20pub enum RoomType {
21 #[ruma_enum(rename = "m.space")]
23 Space,
24
25 #[cfg(feature = "unstable-msc3417")]
30 #[ruma_enum(rename = "org.matrix.msc3417.call")]
31 Call,
32
33 #[doc(hidden)]
35 _Custom(PrivOwnedStr),
36}
37
38#[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 Invite,
50
51 Knock,
55
56 Private,
58
59 Restricted(Restricted),
62
63 KnockRestricted(Restricted),
66
67 Public,
69
70 #[doc(hidden)]
71 _Custom(CustomJoinRule),
72}
73
74impl JoinRule {
75 pub fn kind(&self) -> JoinRuleKind {
77 match self {
78 Self::Invite => JoinRuleKind::Invite,
79 Self::Knock => JoinRuleKind::Knock,
80 Self::Private => JoinRuleKind::Private,
81 Self::Restricted(_) => JoinRuleKind::Restricted,
82 Self::KnockRestricted(_) => JoinRuleKind::KnockRestricted,
83 Self::Public => JoinRuleKind::Public,
84 Self::_Custom(CustomJoinRule { join_rule, .. }) => {
85 JoinRuleKind::_Custom(PrivOwnedStr(join_rule.as_str().into()))
86 }
87 }
88 }
89
90 pub fn as_str(&self) -> &str {
92 match self {
93 JoinRule::Invite => "invite",
94 JoinRule::Knock => "knock",
95 JoinRule::Private => "private",
96 JoinRule::Restricted(_) => "restricted",
97 JoinRule::KnockRestricted(_) => "knock_restricted",
98 JoinRule::Public => "public",
99 JoinRule::_Custom(CustomJoinRule { join_rule, .. }) => join_rule,
100 }
101 }
102
103 pub fn data(&self) -> Cow<'_, JsonObject> {
111 fn serialize<T: Serialize>(obj: &T) -> JsonObject {
112 match serde_json::to_value(obj).expect("join rule serialization should succeed") {
113 JsonValue::Object(mut obj) => {
114 obj.remove("body");
115 obj
116 }
117 _ => panic!("all message types should serialize to objects"),
118 }
119 }
120
121 match self {
122 JoinRule::Invite | JoinRule::Knock | JoinRule::Private | JoinRule::Public => {
123 Cow::Owned(JsonObject::new())
124 }
125 JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => {
126 Cow::Owned(serialize(restricted))
127 }
128 Self::_Custom(c) => Cow::Borrowed(&c.data),
129 }
130 }
131}
132
133impl<'de> Deserialize<'de> for JoinRule {
134 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135 where
136 D: de::Deserializer<'de>,
137 {
138 let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
139
140 #[derive(Deserialize)]
141 struct ExtractType<'a> {
142 #[serde(borrow)]
143 join_rule: Option<Cow<'a, str>>,
144 }
145
146 let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get())
147 .map_err(de::Error::custom)?
148 .join_rule
149 .ok_or_else(|| de::Error::missing_field("join_rule"))?;
150
151 match join_rule.as_ref() {
152 "invite" => Ok(Self::Invite),
153 "knock" => Ok(Self::Knock),
154 "private" => Ok(Self::Private),
155 "restricted" => from_raw_json_value(&json).map(Self::Restricted),
156 "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
157 "public" => Ok(Self::Public),
158 _ => from_raw_json_value(&json).map(Self::_Custom),
159 }
160 }
161}
162
163#[doc(hidden)]
165#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
166pub struct CustomJoinRule {
167 join_rule: String,
169
170 #[serde(flatten)]
172 data: JsonObject,
173}
174
175#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
177#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
178pub struct Restricted {
179 #[serde(default, deserialize_with = "crate::serde::ignore_invalid_vec_items")]
181 pub allow: Vec<AllowRule>,
182}
183
184impl Restricted {
185 pub fn new(allow: Vec<AllowRule>) -> Self {
187 Self { allow }
188 }
189}
190
191#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
193#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
194#[serde(untagged)]
195pub enum AllowRule {
196 RoomMembership(RoomMembership),
198
199 #[doc(hidden)]
200 _Custom(Box<CustomAllowRule>),
201}
202
203impl AllowRule {
204 pub fn room_membership(room_id: OwnedRoomId) -> Self {
206 Self::RoomMembership(RoomMembership::new(room_id))
207 }
208}
209
210#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
212#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
213#[serde(tag = "type", rename = "m.room_membership")]
214pub struct RoomMembership {
215 pub room_id: OwnedRoomId,
217}
218
219impl RoomMembership {
220 pub fn new(room_id: OwnedRoomId) -> Self {
222 Self { room_id }
223 }
224}
225
226#[doc(hidden)]
227#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
228#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
229pub struct CustomAllowRule {
230 #[serde(rename = "type")]
231 rule_type: String,
232 #[serde(flatten)]
233 extra: BTreeMap<String, JsonValue>,
234}
235
236impl<'de> Deserialize<'de> for AllowRule {
237 fn deserialize<D>(deserializer: D) -> Result<AllowRule, D::Error>
238 where
239 D: de::Deserializer<'de>,
240 {
241 let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
242
243 #[derive(Deserialize)]
245 struct ExtractType<'a> {
246 #[serde(borrow, rename = "type")]
247 rule_type: Option<Cow<'a, str>>,
248 }
249
250 let rule_type = serde_json::from_str::<ExtractType<'_>>(json.get())
252 .map_err(de::Error::custom)?
253 .rule_type;
254
255 match rule_type.as_deref() {
256 Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership),
257 Some(_) => from_raw_json_value(&json).map(Self::_Custom),
258 None => Err(de::Error::missing_field("type")),
259 }
260 }
261}
262
263#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
265#[derive(Clone, Default, StringEnum)]
266#[ruma_enum(rename_all = "snake_case")]
267#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
268pub enum JoinRuleKind {
269 Invite,
272
273 Knock,
277
278 Private,
280
281 Restricted,
284
285 KnockRestricted,
288
289 #[default]
291 Public,
292
293 #[doc(hidden)]
294 _Custom(PrivOwnedStr),
295}
296
297impl From<JoinRuleKind> for JoinRuleSummary {
298 fn from(value: JoinRuleKind) -> Self {
299 match value {
300 JoinRuleKind::Invite => Self::Invite,
301 JoinRuleKind::Knock => Self::Knock,
302 JoinRuleKind::Private => Self::Private,
303 JoinRuleKind::Restricted => Self::Restricted(Default::default()),
304 JoinRuleKind::KnockRestricted => Self::KnockRestricted(Default::default()),
305 JoinRuleKind::Public => Self::Public,
306 JoinRuleKind::_Custom(s) => Self::_Custom(s),
307 }
308 }
309}
310
311#[derive(Debug, Clone, Serialize)]
313#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
314pub struct RoomSummary {
315 pub room_id: OwnedRoomId,
317
318 #[serde(skip_serializing_if = "Option::is_none")]
323 pub canonical_alias: Option<OwnedRoomAliasId>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub name: Option<String>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub topic: Option<String>,
332
333 #[serde(skip_serializing_if = "Option::is_none")]
338 pub avatar_url: Option<OwnedMxcUri>,
339
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub room_type: Option<RoomType>,
343
344 pub num_joined_members: UInt,
346
347 #[serde(flatten, skip_serializing_if = "ruma_common::serde::is_default")]
349 pub join_rule: JoinRuleSummary,
350
351 pub world_readable: bool,
353
354 pub guest_can_join: bool,
358
359 #[serde(skip_serializing_if = "Option::is_none")]
361 pub encryption: Option<EventEncryptionAlgorithm>,
362
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub room_version: Option<RoomVersionId>,
366}
367
368impl RoomSummary {
369 pub fn new(
371 room_id: OwnedRoomId,
372 join_rule: JoinRuleSummary,
373 guest_can_join: bool,
374 num_joined_members: UInt,
375 world_readable: bool,
376 ) -> Self {
377 Self {
378 room_id,
379 canonical_alias: None,
380 name: None,
381 topic: None,
382 avatar_url: None,
383 room_type: None,
384 num_joined_members,
385 join_rule,
386 world_readable,
387 guest_can_join,
388 encryption: None,
389 room_version: None,
390 }
391 }
392}
393
394impl<'de> Deserialize<'de> for RoomSummary {
395 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
396 where
397 D: de::Deserializer<'de>,
398 {
399 #[derive(Deserialize)]
402 struct RoomSummaryDeHelper {
403 room_id: OwnedRoomId,
404 #[cfg_attr(
405 feature = "compat-empty-string-null",
406 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
407 )]
408 canonical_alias: Option<OwnedRoomAliasId>,
409 name: Option<String>,
410 topic: Option<String>,
411 #[cfg_attr(
412 feature = "compat-empty-string-null",
413 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
414 )]
415 avatar_url: Option<OwnedMxcUri>,
416 room_type: Option<RoomType>,
417 num_joined_members: UInt,
418 world_readable: bool,
419 guest_can_join: bool,
420 encryption: Option<EventEncryptionAlgorithm>,
421 room_version: Option<RoomVersionId>,
422 }
423
424 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
425 let RoomSummaryDeHelper {
426 room_id,
427 canonical_alias,
428 name,
429 topic,
430 avatar_url,
431 room_type,
432 num_joined_members,
433 world_readable,
434 guest_can_join,
435 encryption,
436 room_version,
437 } = from_raw_json_value(&json)?;
438 let join_rule: JoinRuleSummary = from_raw_json_value(&json)?;
439
440 Ok(Self {
441 room_id,
442 canonical_alias,
443 name,
444 topic,
445 avatar_url,
446 room_type,
447 num_joined_members,
448 join_rule,
449 world_readable,
450 guest_can_join,
451 encryption,
452 room_version,
453 })
454 }
455}
456
457#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
471#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
472#[serde(tag = "join_rule", rename_all = "snake_case")]
473pub enum JoinRuleSummary {
474 Invite,
477
478 Knock,
482
483 Private,
485
486 Restricted(RestrictedSummary),
489
490 KnockRestricted(RestrictedSummary),
493
494 #[default]
496 Public,
497
498 #[doc(hidden)]
499 #[serde(skip_serializing)]
500 _Custom(PrivOwnedStr),
501}
502
503impl JoinRuleSummary {
504 pub fn kind(&self) -> JoinRuleKind {
506 match self {
507 Self::Invite => JoinRuleKind::Invite,
508 Self::Knock => JoinRuleKind::Knock,
509 Self::Private => JoinRuleKind::Private,
510 Self::Restricted(_) => JoinRuleKind::Restricted,
511 Self::KnockRestricted(_) => JoinRuleKind::KnockRestricted,
512 Self::Public => JoinRuleKind::Public,
513 Self::_Custom(rule) => JoinRuleKind::_Custom(rule.clone()),
514 }
515 }
516
517 pub fn as_str(&self) -> &str {
519 match self {
520 Self::Invite => "invite",
521 Self::Knock => "knock",
522 Self::Private => "private",
523 Self::Restricted(_) => "restricted",
524 Self::KnockRestricted(_) => "knock_restricted",
525 Self::Public => "public",
526 Self::_Custom(rule) => &rule.0,
527 }
528 }
529}
530
531impl From<JoinRule> for JoinRuleSummary {
532 fn from(value: JoinRule) -> Self {
533 match value {
534 JoinRule::Invite => Self::Invite,
535 JoinRule::Knock => Self::Knock,
536 JoinRule::Private => Self::Private,
537 JoinRule::Restricted(restricted) => Self::Restricted(restricted.into()),
538 JoinRule::KnockRestricted(restricted) => Self::KnockRestricted(restricted.into()),
539 JoinRule::Public => Self::Public,
540 JoinRule::_Custom(CustomJoinRule { join_rule, .. }) => {
541 Self::_Custom(PrivOwnedStr(join_rule.into()))
542 }
543 }
544 }
545}
546
547impl<'de> Deserialize<'de> for JoinRuleSummary {
548 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
549 where
550 D: de::Deserializer<'de>,
551 {
552 let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
553
554 #[derive(Deserialize)]
555 struct ExtractType<'a> {
556 #[serde(borrow)]
557 join_rule: Option<Cow<'a, str>>,
558 }
559
560 let Some(join_rule) = serde_json::from_str::<ExtractType<'_>>(json.get())
561 .map_err(de::Error::custom)?
562 .join_rule
563 else {
564 return Ok(Self::default());
565 };
566
567 match join_rule.as_ref() {
568 "invite" => Ok(Self::Invite),
569 "knock" => Ok(Self::Knock),
570 "private" => Ok(Self::Private),
571 "restricted" => from_raw_json_value(&json).map(Self::Restricted),
572 "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
573 "public" => Ok(Self::Public),
574 _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
575 }
576 }
577}
578
579#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
581#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
582pub struct RestrictedSummary {
583 #[serde(default)]
585 pub allowed_room_ids: Vec<OwnedRoomId>,
586}
587
588impl RestrictedSummary {
589 pub fn new(allowed_room_ids: Vec<OwnedRoomId>) -> Self {
591 Self { allowed_room_ids }
592 }
593}
594
595impl From<Restricted> for RestrictedSummary {
596 fn from(value: Restricted) -> Self {
597 let allowed_room_ids = value
598 .allow
599 .into_iter()
600 .filter_map(|allow_rule| {
601 let membership = as_variant!(allow_rule, AllowRule::RoomMembership)?;
602 Some(membership.room_id)
603 })
604 .collect();
605
606 Self::new(allowed_room_ids)
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use std::collections::BTreeMap;
613
614 use assert_matches2::assert_matches;
615 use js_int::uint;
616 use ruma_common::{OwnedRoomId, owned_room_id};
617 use serde_json::{from_value as from_json_value, json};
618
619 use super::{
620 AllowRule, CustomAllowRule, JoinRule, JoinRuleSummary, Restricted, RestrictedSummary,
621 RoomMembership, RoomSummary,
622 };
623 use crate::assert_to_canonical_json_eq;
624
625 #[test]
626 fn deserialize_summary_no_join_rule() {
627 let json = json!({
628 "room_id": "!room:localhost",
629 "num_joined_members": 5,
630 "world_readable": false,
631 "guest_can_join": false,
632 });
633
634 let summary: RoomSummary = from_json_value(json).unwrap();
635 assert_eq!(summary.room_id, "!room:localhost");
636 assert_eq!(summary.num_joined_members, uint!(5));
637 assert!(!summary.world_readable);
638 assert!(!summary.guest_can_join);
639 assert_matches!(summary.join_rule, JoinRuleSummary::Public);
640 }
641
642 #[test]
643 fn deserialize_summary_private_join_rule() {
644 let json = json!({
645 "room_id": "!room:localhost",
646 "num_joined_members": 5,
647 "world_readable": false,
648 "guest_can_join": false,
649 "join_rule": "private",
650 });
651
652 let summary: RoomSummary = from_json_value(json).unwrap();
653 assert_eq!(summary.room_id, "!room:localhost");
654 assert_eq!(summary.num_joined_members, uint!(5));
655 assert!(!summary.world_readable);
656 assert!(!summary.guest_can_join);
657 assert_matches!(summary.join_rule, JoinRuleSummary::Private);
658 }
659
660 #[test]
661 fn deserialize_summary_restricted_join_rule() {
662 let json = json!({
663 "room_id": "!room:localhost",
664 "num_joined_members": 5,
665 "world_readable": false,
666 "guest_can_join": false,
667 "join_rule": "restricted",
668 "allowed_room_ids": ["!otherroom:localhost"],
669 });
670
671 let summary: RoomSummary = from_json_value(json).unwrap();
672 assert_eq!(summary.room_id, "!room:localhost");
673 assert_eq!(summary.num_joined_members, uint!(5));
674 assert!(!summary.world_readable);
675 assert!(!summary.guest_can_join);
676 assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
677 assert_eq!(restricted.allowed_room_ids.len(), 1);
678 }
679
680 #[test]
681 fn deserialize_summary_restricted_join_rule_no_allowed_room_ids() {
682 let json = json!({
683 "room_id": "!room:localhost",
684 "num_joined_members": 5,
685 "world_readable": false,
686 "guest_can_join": false,
687 "join_rule": "restricted",
688 });
689
690 let summary: RoomSummary = from_json_value(json).unwrap();
691 assert_eq!(summary.room_id, "!room:localhost");
692 assert_eq!(summary.num_joined_members, uint!(5));
693 assert!(!summary.world_readable);
694 assert!(!summary.guest_can_join);
695 assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
696 assert_eq!(restricted.allowed_room_ids.len(), 0);
697 }
698
699 #[test]
700 fn serialize_summary_knock_join_rule() {
701 let summary = RoomSummary::new(
702 owned_room_id!("!room:localhost"),
703 JoinRuleSummary::Knock,
704 false,
705 uint!(5),
706 false,
707 );
708
709 assert_to_canonical_json_eq!(
710 summary,
711 json!({
712 "room_id": "!room:localhost",
713 "num_joined_members": 5,
714 "world_readable": false,
715 "guest_can_join": false,
716 "join_rule": "knock",
717 })
718 );
719 }
720
721 #[test]
722 fn serialize_summary_restricted_join_rule() {
723 let summary = RoomSummary::new(
724 owned_room_id!("!room:localhost"),
725 JoinRuleSummary::Restricted(RestrictedSummary::new(vec![owned_room_id!(
726 "!otherroom:localhost"
727 )])),
728 false,
729 uint!(5),
730 false,
731 );
732
733 assert_to_canonical_json_eq!(
734 summary,
735 json!({
736 "room_id": "!room:localhost",
737 "num_joined_members": 5,
738 "world_readable": false,
739 "guest_can_join": false,
740 "join_rule": "restricted",
741 "allowed_room_ids": ["!otherroom:localhost"],
742 })
743 );
744 }
745
746 #[test]
747 fn join_rule_to_join_rule_summary() {
748 assert_eq!(JoinRuleSummary::Invite, JoinRule::Invite.into());
749 assert_eq!(JoinRuleSummary::Knock, JoinRule::Knock.into());
750 assert_eq!(JoinRuleSummary::Public, JoinRule::Public.into());
751 assert_eq!(JoinRuleSummary::Private, JoinRule::Private.into());
752
753 assert_matches!(
754 JoinRule::KnockRestricted(Restricted::default()).into(),
755 JoinRuleSummary::KnockRestricted(restricted)
756 );
757 assert_eq!(restricted.allowed_room_ids, &[] as &[OwnedRoomId]);
758
759 let room_id = owned_room_id!("!room:localhost");
760 assert_matches!(
761 JoinRule::Restricted(Restricted::new(vec![AllowRule::RoomMembership(
762 RoomMembership::new(room_id.clone())
763 )]))
764 .into(),
765 JoinRuleSummary::Restricted(restricted)
766 );
767 assert_eq!(restricted.allowed_room_ids, [room_id]);
768 }
769
770 #[test]
771 fn roundtrip_custom_allow_rule() {
772 let json = r#"{"type":"org.msc9000.something","foo":"bar"}"#;
773 let allow_rule: AllowRule = serde_json::from_str(json).unwrap();
774 assert_matches!(&allow_rule, AllowRule::_Custom(_));
775 assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json);
776 }
777
778 #[test]
779 fn invalid_allow_items() {
780 let json = r#"{
781 "join_rule": "restricted",
782 "allow": [
783 {
784 "type": "m.room_membership",
785 "room_id": "!mods:example.org"
786 },
787 {
788 "type": "m.room_membership",
789 "room_id": ""
790 },
791 {
792 "type": "m.room_membership",
793 "room_id": "not a room id"
794 },
795 {
796 "type": "org.example.custom",
797 "org.example.minimum_role": "developer"
798 },
799 {
800 "not even close": "to being correct",
801 "any object": "passes this test",
802 "only non-objects in this array": "cause deserialization to fail"
803 }
804 ]
805 }"#;
806 let join_rule: JoinRule = serde_json::from_str(json).unwrap();
807
808 assert_matches!(join_rule, JoinRule::Restricted(restricted));
809 assert_eq!(
810 restricted.allow,
811 &[
812 AllowRule::room_membership(owned_room_id!("!mods:example.org")),
813 AllowRule::_Custom(Box::new(CustomAllowRule {
814 rule_type: "org.example.custom".into(),
815 extra: BTreeMap::from([(
816 "org.example.minimum_role".into(),
817 "developer".into()
818 )])
819 }))
820 ]
821 );
822 }
823}