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}