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 #[doc(hidden)]
27 _Custom(PrivOwnedStr),
28}
29
30#[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 Invite,
42
43 Knock,
47
48 Private,
50
51 Restricted(Restricted),
54
55 KnockRestricted(Restricted),
58
59 Public,
61
62 #[doc(hidden)]
63 _Custom(CustomJoinRule),
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(CustomJoinRule { join_rule, .. }) => {
77 JoinRuleKind::_Custom(PrivOwnedStr(join_rule.as_str().into()))
78 }
79 }
80 }
81
82 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 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#[doc(hidden)]
157#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
158pub struct CustomJoinRule {
159 join_rule: String,
161
162 #[serde(flatten)]
164 data: JsonObject,
165}
166
167#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
169#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
170pub struct Restricted {
171 #[serde(default, deserialize_with = "crate::serde::ignore_invalid_vec_items")]
173 pub allow: Vec<AllowRule>,
174}
175
176impl Restricted {
177 pub fn new(allow: Vec<AllowRule>) -> Self {
179 Self { allow }
180 }
181}
182
183#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
185#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
186#[serde(untagged)]
187pub enum AllowRule {
188 RoomMembership(RoomMembership),
190
191 #[doc(hidden)]
192 _Custom(Box<CustomAllowRule>),
193}
194
195impl AllowRule {
196 pub fn room_membership(room_id: OwnedRoomId) -> Self {
198 Self::RoomMembership(RoomMembership::new(room_id))
199 }
200}
201
202#[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 pub room_id: OwnedRoomId,
209}
210
211impl RoomMembership {
212 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 #[derive(Deserialize)]
237 struct ExtractType<'a> {
238 #[serde(borrow, rename = "type")]
239 rule_type: Option<Cow<'a, str>>,
240 }
241
242 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#[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 Invite,
264
265 Knock,
269
270 Private,
272
273 Restricted,
276
277 KnockRestricted,
280
281 #[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#[derive(Debug, Clone, Serialize)]
305#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
306pub struct RoomSummary {
307 pub room_id: OwnedRoomId,
309
310 #[serde(skip_serializing_if = "Option::is_none")]
315 pub canonical_alias: Option<OwnedRoomAliasId>,
316
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub name: Option<String>,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub topic: Option<String>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
330 pub avatar_url: Option<OwnedMxcUri>,
331
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub room_type: Option<RoomType>,
335
336 pub num_joined_members: UInt,
338
339 #[serde(flatten, skip_serializing_if = "ruma_common::serde::is_default")]
341 pub join_rule: JoinRuleSummary,
342
343 pub world_readable: bool,
345
346 pub guest_can_join: bool,
350
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub encryption: Option<EventEncryptionAlgorithm>,
354
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub room_version: Option<RoomVersionId>,
358}
359
360impl RoomSummary {
361 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 #[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#[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 Invite,
469
470 Knock,
474
475 Private,
477
478 Restricted(RestrictedSummary),
481
482 KnockRestricted(RestrictedSummary),
485
486 #[default]
488 Public,
489
490 #[doc(hidden)]
491 #[serde(skip_serializing)]
492 _Custom(PrivOwnedStr),
493}
494
495impl JoinRuleSummary {
496 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 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#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
573#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
574pub struct RestrictedSummary {
575 #[serde(default)]
577 pub allowed_room_ids: Vec<OwnedRoomId>,
578}
579
580impl RestrictedSummary {
581 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}