ruma_common/
room.rs

1//! Common types for rooms.
2
3use std::borrow::Cow;
4
5use js_int::UInt;
6use serde::{de, Deserialize, Serialize};
7use serde_json::value::RawValue as RawJsonValue;
8
9use crate::{
10    directory::PublicRoomJoinRule,
11    serde::{from_raw_json_value, StringEnum},
12    space::SpaceRoomJoinRule,
13    EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr,
14    RoomVersionId,
15};
16
17/// An enum of possible room types.
18#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
19#[derive(Clone, PartialEq, Eq, StringEnum)]
20#[non_exhaustive]
21pub enum RoomType {
22    /// Defines the room as a space.
23    #[ruma_enum(rename = "m.space")]
24    Space,
25
26    /// Defines the room as a custom type.
27    #[doc(hidden)]
28    _Custom(PrivOwnedStr),
29}
30
31/// The summary of a room's state.
32#[derive(Debug, Clone, Serialize)]
33#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
34pub struct RoomSummary {
35    /// The ID of the room.
36    pub room_id: OwnedRoomId,
37
38    /// The canonical alias of the room, if any.
39    ///
40    /// If the `compat-empty-string-null` cargo feature is enabled, this field being an empty
41    /// string in JSON will result in `None` here during deserialization.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    #[cfg_attr(
44        feature = "compat-empty-string-null",
45        serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
46    )]
47    pub canonical_alias: Option<OwnedRoomAliasId>,
48
49    /// The name of the room, if any.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub name: Option<String>,
52
53    /// The topic of the room, if any.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub topic: Option<String>,
56
57    /// The URL for the room's avatar, if one is set.
58    ///
59    /// If you activate the `compat-empty-string-null` feature, this field being an empty string in
60    /// JSON will result in `None` here during deserialization.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    #[cfg_attr(
63        feature = "compat-empty-string-null",
64        serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
65    )]
66    pub avatar_url: Option<OwnedMxcUri>,
67
68    /// The type of room from `m.room.create`, if any.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub room_type: Option<RoomType>,
71
72    /// The number of members joined to the room.
73    pub num_joined_members: UInt,
74
75    /// The join rule of the room.
76    #[serde(flatten, skip_serializing_if = "ruma_common::serde::is_default")]
77    pub join_rule: JoinRuleSummary,
78
79    /// Whether the room may be viewed by users without joining.
80    pub world_readable: bool,
81
82    /// Whether guest users may join the room and participate in it.
83    ///
84    /// If they can, they will be subject to ordinary power level rules like any other user.
85    pub guest_can_join: bool,
86
87    /// If the room is encrypted, the algorithm used for this room.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub encryption: Option<EventEncryptionAlgorithm>,
90
91    /// The version of the room.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub room_version: Option<RoomVersionId>,
94}
95
96impl RoomSummary {
97    /// Construct a new `RoomSummary` with the given required fields.
98    pub fn new(
99        room_id: OwnedRoomId,
100        join_rule: JoinRuleSummary,
101        guest_can_join: bool,
102        num_joined_members: UInt,
103        world_readable: bool,
104    ) -> Self {
105        Self {
106            room_id,
107            canonical_alias: None,
108            name: None,
109            topic: None,
110            avatar_url: None,
111            room_type: None,
112            num_joined_members,
113            join_rule,
114            world_readable,
115            guest_can_join,
116            encryption: None,
117            room_version: None,
118        }
119    }
120}
121
122impl<'de> Deserialize<'de> for RoomSummary {
123    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124    where
125        D: de::Deserializer<'de>,
126    {
127        /// Helper type to deserialize [`RoomSummary`] because using `flatten` on `join_rule`
128        /// returns an error.
129        #[derive(Deserialize)]
130        struct RoomSummaryDeHelper {
131            room_id: OwnedRoomId,
132            #[cfg_attr(
133                feature = "compat-empty-string-null",
134                serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
135            )]
136            canonical_alias: Option<OwnedRoomAliasId>,
137            name: Option<String>,
138            topic: Option<String>,
139            #[cfg_attr(
140                feature = "compat-empty-string-null",
141                serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
142            )]
143            avatar_url: Option<OwnedMxcUri>,
144            room_type: Option<RoomType>,
145            num_joined_members: UInt,
146            world_readable: bool,
147            guest_can_join: bool,
148            encryption: Option<EventEncryptionAlgorithm>,
149            room_version: Option<RoomVersionId>,
150        }
151
152        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
153        let RoomSummaryDeHelper {
154            room_id,
155            canonical_alias,
156            name,
157            topic,
158            avatar_url,
159            room_type,
160            num_joined_members,
161            world_readable,
162            guest_can_join,
163            encryption,
164            room_version,
165        } = from_raw_json_value(&json)?;
166        let join_rule: JoinRuleSummary = from_raw_json_value(&json)?;
167
168        Ok(Self {
169            room_id,
170            canonical_alias,
171            name,
172            topic,
173            avatar_url,
174            room_type,
175            num_joined_members,
176            join_rule,
177            world_readable,
178            guest_can_join,
179            encryption,
180            room_version,
181        })
182    }
183}
184
185/// The rule used for users wishing to join a room.
186///
187/// In contrast to the regular `JoinRule` in `ruma_events`, this enum does holds only simplified
188/// conditions for joining restricted rooms.
189#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
190#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
191#[serde(tag = "join_rule", rename_all = "snake_case")]
192pub enum JoinRuleSummary {
193    /// A user who wishes to join the room must first receive an invite to the room from someone
194    /// already inside of the room.
195    Invite,
196
197    /// Users can join the room if they are invited, or they can request an invite to the room.
198    ///
199    /// They can be allowed (invited) or denied (kicked/banned) access.
200    Knock,
201
202    /// Reserved but not yet implemented by the Matrix specification.
203    Private,
204
205    /// Users can join the room if they are invited, or if they meet one of the conditions
206    /// described in the [`RestrictedSummary`].
207    Restricted(RestrictedSummary),
208
209    /// Users can join the room if they are invited, or if they meet one of the conditions
210    /// described in the [`RestrictedSummary`], or they can request an invite to the room.
211    KnockRestricted(RestrictedSummary),
212
213    /// Anyone can join the room without any prior action.
214    #[default]
215    Public,
216
217    #[doc(hidden)]
218    #[serde(skip_serializing)]
219    _Custom(PrivOwnedStr),
220}
221
222impl JoinRuleSummary {
223    /// Returns the string name of this `JoinRule`.
224    pub fn as_str(&self) -> &str {
225        match self {
226            Self::Invite => "invite",
227            Self::Knock => "knock",
228            Self::Private => "private",
229            Self::Restricted(_) => "restricted",
230            Self::KnockRestricted(_) => "knock_restricted",
231            Self::Public => "public",
232            Self::_Custom(rule) => &rule.0,
233        }
234    }
235}
236
237impl From<JoinRuleSummary> for PublicRoomJoinRule {
238    fn from(value: JoinRuleSummary) -> Self {
239        match value {
240            JoinRuleSummary::Invite => Self::Invite,
241            JoinRuleSummary::Knock => Self::Knock,
242            JoinRuleSummary::Private => Self::Private,
243            JoinRuleSummary::Restricted(_) => Self::Restricted,
244            JoinRuleSummary::KnockRestricted(_) => Self::KnockRestricted,
245            JoinRuleSummary::Public => Self::Public,
246            JoinRuleSummary::_Custom(custom) => Self::_Custom(custom),
247        }
248    }
249}
250
251impl From<JoinRuleSummary> for SpaceRoomJoinRule {
252    fn from(value: JoinRuleSummary) -> Self {
253        match value {
254            JoinRuleSummary::Invite => Self::Invite,
255            JoinRuleSummary::Knock => Self::Knock,
256            JoinRuleSummary::Private => Self::Private,
257            JoinRuleSummary::Restricted(_) => Self::Restricted,
258            JoinRuleSummary::KnockRestricted(_) => Self::KnockRestricted,
259            JoinRuleSummary::Public => Self::Public,
260            JoinRuleSummary::_Custom(custom) => Self::_Custom(custom),
261        }
262    }
263}
264
265impl<'de> Deserialize<'de> for JoinRuleSummary {
266    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
267    where
268        D: de::Deserializer<'de>,
269    {
270        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
271
272        #[derive(Deserialize)]
273        struct ExtractType<'a> {
274            #[serde(borrow)]
275            join_rule: Option<Cow<'a, str>>,
276        }
277
278        let Some(join_rule) = serde_json::from_str::<ExtractType<'_>>(json.get())
279            .map_err(de::Error::custom)?
280            .join_rule
281        else {
282            return Ok(Self::default());
283        };
284
285        match join_rule.as_ref() {
286            "invite" => Ok(Self::Invite),
287            "knock" => Ok(Self::Knock),
288            "private" => Ok(Self::Private),
289            "restricted" => from_raw_json_value(&json).map(Self::Restricted),
290            "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
291            "public" => Ok(Self::Public),
292            _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
293        }
294    }
295}
296
297/// A summary of the conditions for joining a restricted room.
298#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
299#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
300pub struct RestrictedSummary {
301    /// The room IDs which are specified by the join rules.
302    #[serde(default)]
303    pub allowed_room_ids: Vec<OwnedRoomId>,
304}
305
306impl RestrictedSummary {
307    /// Constructs a new `RestrictedSummary` with the given room IDs.
308    pub fn new(allowed_room_ids: Vec<OwnedRoomId>) -> Self {
309        Self { allowed_room_ids }
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use assert_matches2::assert_matches;
316    use js_int::uint;
317    use ruma_common::owned_room_id;
318    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
319
320    use super::{JoinRuleSummary, RestrictedSummary, RoomSummary};
321
322    #[test]
323    fn deserialize_summary_no_join_rule() {
324        let json = json!({
325            "room_id": "!room:localhost",
326            "num_joined_members": 5,
327            "world_readable": false,
328            "guest_can_join": false,
329        });
330
331        let summary: RoomSummary = from_json_value(json).unwrap();
332        assert_eq!(summary.room_id, "!room:localhost");
333        assert_eq!(summary.num_joined_members, uint!(5));
334        assert!(!summary.world_readable);
335        assert!(!summary.guest_can_join);
336        assert_matches!(summary.join_rule, JoinRuleSummary::Public);
337    }
338
339    #[test]
340    fn deserialize_summary_private_join_rule() {
341        let json = json!({
342            "room_id": "!room:localhost",
343            "num_joined_members": 5,
344            "world_readable": false,
345            "guest_can_join": false,
346            "join_rule": "private",
347        });
348
349        let summary: RoomSummary = from_json_value(json).unwrap();
350        assert_eq!(summary.room_id, "!room:localhost");
351        assert_eq!(summary.num_joined_members, uint!(5));
352        assert!(!summary.world_readable);
353        assert!(!summary.guest_can_join);
354        assert_matches!(summary.join_rule, JoinRuleSummary::Private);
355    }
356
357    #[test]
358    fn deserialize_summary_restricted_join_rule() {
359        let json = json!({
360            "room_id": "!room:localhost",
361            "num_joined_members": 5,
362            "world_readable": false,
363            "guest_can_join": false,
364            "join_rule": "restricted",
365            "allowed_room_ids": ["!otherroom:localhost"],
366        });
367
368        let summary: RoomSummary = from_json_value(json).unwrap();
369        assert_eq!(summary.room_id, "!room:localhost");
370        assert_eq!(summary.num_joined_members, uint!(5));
371        assert!(!summary.world_readable);
372        assert!(!summary.guest_can_join);
373        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
374        assert_eq!(restricted.allowed_room_ids.len(), 1);
375    }
376
377    #[test]
378    fn deserialize_summary_restricted_join_rule_no_allowed_room_ids() {
379        let json = json!({
380            "room_id": "!room:localhost",
381            "num_joined_members": 5,
382            "world_readable": false,
383            "guest_can_join": false,
384            "join_rule": "restricted",
385        });
386
387        let summary: RoomSummary = from_json_value(json).unwrap();
388        assert_eq!(summary.room_id, "!room:localhost");
389        assert_eq!(summary.num_joined_members, uint!(5));
390        assert!(!summary.world_readable);
391        assert!(!summary.guest_can_join);
392        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
393        assert_eq!(restricted.allowed_room_ids.len(), 0);
394    }
395
396    #[test]
397    fn serialize_summary_knock_join_rule() {
398        let summary = RoomSummary::new(
399            owned_room_id!("!room:localhost"),
400            JoinRuleSummary::Knock,
401            false,
402            uint!(5),
403            false,
404        );
405
406        assert_eq!(
407            to_json_value(&summary).unwrap(),
408            json!({
409                "room_id": "!room:localhost",
410                "num_joined_members": 5,
411                "world_readable": false,
412                "guest_can_join": false,
413                "join_rule": "knock",
414            })
415        );
416    }
417
418    #[test]
419    fn serialize_summary_restricted_join_rule() {
420        let summary = RoomSummary::new(
421            owned_room_id!("!room:localhost"),
422            JoinRuleSummary::Restricted(RestrictedSummary::new(vec![owned_room_id!(
423                "!otherroom:localhost"
424            )])),
425            false,
426            uint!(5),
427            false,
428        );
429
430        assert_eq!(
431            to_json_value(&summary).unwrap(),
432            json!({
433                "room_id": "!room:localhost",
434                "num_joined_members": 5,
435                "world_readable": false,
436                "guest_can_join": false,
437                "join_rule": "restricted",
438                "allowed_room_ids": ["!otherroom:localhost"],
439            })
440        );
441    }
442}