ruma_common/
directory.rs

1//! Common types for room directory endpoints.
2
3use js_int::UInt;
4use serde::{Deserialize, Serialize};
5
6mod filter_room_type_serde;
7mod room_network_serde;
8
9use crate::{
10    room::{RoomSummary, RoomType},
11    serde::StringEnum,
12    space::SpaceRoomJoinRule,
13    OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr,
14};
15
16/// A chunk of a room list response, describing one room.
17///
18/// To create an instance of this type, first create a [`PublicRoomsChunkInit`] and convert it via
19/// `PublicRoomsChunk::from` / `.into()`. It is also possible to construct this type from a
20/// [`RoomSummary`].
21#[derive(Clone, Debug, Deserialize, Serialize)]
22#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
23pub struct PublicRoomsChunk {
24    /// The canonical alias of the room, if any.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    #[cfg_attr(
27        feature = "compat-empty-string-null",
28        serde(default, deserialize_with = "crate::serde::empty_string_as_none")
29    )]
30    pub canonical_alias: Option<OwnedRoomAliasId>,
31
32    /// The name of the room, if any.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub name: Option<String>,
35
36    /// The number of members joined to the room.
37    pub num_joined_members: UInt,
38
39    /// The ID of the room.
40    pub room_id: OwnedRoomId,
41
42    /// The topic of the room, if any.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub topic: Option<String>,
45
46    /// Whether the room may be viewed by guest users without joining.
47    pub world_readable: bool,
48
49    /// Whether guest users may join the room and participate in it.
50    ///
51    /// If they can, they will be subject to ordinary power level rules like any other user.
52    pub guest_can_join: bool,
53
54    /// The URL for the room's avatar, if one is set.
55    ///
56    /// If you activate the `compat-empty-string-null` feature, this field being an empty string in
57    /// JSON will result in `None` here during deserialization.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    #[cfg_attr(
60        feature = "compat-empty-string-null",
61        serde(default, deserialize_with = "crate::serde::empty_string_as_none")
62    )]
63    pub avatar_url: Option<OwnedMxcUri>,
64
65    /// The join rule of the room.
66    #[serde(default, skip_serializing_if = "crate::serde::is_default")]
67    pub join_rule: PublicRoomJoinRule,
68
69    /// The type of room from `m.room.create`, if any.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub room_type: Option<RoomType>,
72}
73
74/// Initial set of mandatory fields of `PublicRoomsChunk`.
75///
76/// This struct will not be updated even if additional fields are added to `PublicRoomsChunk` in a
77/// new (non-breaking) release of the Matrix specification.
78#[derive(Debug)]
79#[allow(clippy::exhaustive_structs)]
80pub struct PublicRoomsChunkInit {
81    /// The number of members joined to the room.
82    pub num_joined_members: UInt,
83
84    /// The ID of the room.
85    pub room_id: OwnedRoomId,
86
87    /// Whether the room may be viewed by guest users without joining.
88    pub world_readable: bool,
89
90    /// Whether guest users may join the room and participate in it.
91    ///
92    /// If they can, they will be subject to ordinary power level rules like any other user.
93    pub guest_can_join: bool,
94}
95
96impl From<PublicRoomsChunkInit> for PublicRoomsChunk {
97    fn from(init: PublicRoomsChunkInit) -> Self {
98        let PublicRoomsChunkInit { num_joined_members, room_id, world_readable, guest_can_join } =
99            init;
100
101        Self {
102            canonical_alias: None,
103            name: None,
104            num_joined_members,
105            room_id,
106            topic: None,
107            world_readable,
108            guest_can_join,
109            avatar_url: None,
110            join_rule: PublicRoomJoinRule::default(),
111            room_type: None,
112        }
113    }
114}
115
116impl From<RoomSummary> for PublicRoomsChunk {
117    fn from(value: RoomSummary) -> Self {
118        let RoomSummary {
119            room_id,
120            canonical_alias,
121            name,
122            topic,
123            avatar_url,
124            room_type,
125            num_joined_members,
126            join_rule,
127            world_readable,
128            guest_can_join,
129            ..
130        } = value;
131
132        Self {
133            canonical_alias,
134            name,
135            num_joined_members,
136            room_id,
137            topic,
138            world_readable,
139            guest_can_join,
140            avatar_url,
141            join_rule: join_rule.into(),
142            room_type,
143        }
144    }
145}
146
147/// A filter for public rooms lists.
148#[derive(Clone, Debug, Default, Deserialize, Serialize)]
149#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
150pub struct Filter {
151    /// A string to search for in the room metadata, e.g. name, topic, canonical alias etc.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub generic_search_term: Option<String>,
154
155    /// The room types to include in the results.
156    ///
157    /// Includes all room types if it is empty.
158    ///
159    /// If the `compat-null` feature is enabled, a `null` value is allowed in deserialization, and
160    /// treated the same way as an empty list.
161    #[serde(default, skip_serializing_if = "Vec::is_empty")]
162    #[cfg_attr(feature = "compat-null", serde(deserialize_with = "crate::serde::none_as_default"))]
163    pub room_types: Vec<RoomTypeFilter>,
164}
165
166impl Filter {
167    /// Creates an empty `Filter`.
168    pub fn new() -> Self {
169        Default::default()
170    }
171
172    /// Returns `true` if the filter is empty.
173    pub fn is_empty(&self) -> bool {
174        self.generic_search_term.is_none()
175    }
176}
177
178/// Information about which networks/protocols from application services on the
179/// homeserver from which to request rooms.
180#[derive(Clone, Debug, Default, PartialEq, Eq)]
181#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
182pub enum RoomNetwork {
183    /// Return rooms from the Matrix network.
184    #[default]
185    Matrix,
186
187    /// Return rooms from all the networks/protocols the homeserver knows about.
188    All,
189
190    /// Return rooms from a specific third party network/protocol.
191    ThirdParty(String),
192}
193
194/// The rule used for users wishing to join a public room.
195#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
196#[derive(Clone, Default, PartialEq, Eq, StringEnum)]
197#[ruma_enum(rename_all = "snake_case")]
198#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
199pub enum PublicRoomJoinRule {
200    /// A user who wishes to join the room must first receive an invite to the room from someone
201    /// already inside of the room.
202    Invite,
203
204    /// Users can join the room if they are invited, or they can request an invite to the room.
205    ///
206    /// They can be allowed (invited) or denied (kicked/banned) access.
207    Knock,
208
209    /// Reserved but not yet implemented by the Matrix specification.
210    Private,
211
212    /// Users can join the room if they are invited, or if they meet any of the conditions
213    /// described in a set of rules.
214    Restricted,
215
216    /// Users can join the room if they are invited, or if they meet any of the conditions
217    /// described in a set of rules, or they can request an invite to the room.
218    KnockRestricted,
219
220    /// Anyone can join the room without any prior action.
221    #[default]
222    Public,
223
224    #[doc(hidden)]
225    _Custom(PrivOwnedStr),
226}
227
228impl From<PublicRoomJoinRule> for SpaceRoomJoinRule {
229    fn from(value: PublicRoomJoinRule) -> Self {
230        match value {
231            PublicRoomJoinRule::Invite => Self::Invite,
232            PublicRoomJoinRule::Knock => Self::Knock,
233            PublicRoomJoinRule::Private => Self::Private,
234            PublicRoomJoinRule::Restricted => Self::Restricted,
235            PublicRoomJoinRule::KnockRestricted => Self::KnockRestricted,
236            PublicRoomJoinRule::Public => Self::Public,
237            PublicRoomJoinRule::_Custom(custom) => Self::_Custom(custom),
238        }
239    }
240}
241
242impl From<SpaceRoomJoinRule> for PublicRoomJoinRule {
243    fn from(value: SpaceRoomJoinRule) -> Self {
244        match value {
245            SpaceRoomJoinRule::Invite => Self::Invite,
246            SpaceRoomJoinRule::Knock => Self::Knock,
247            SpaceRoomJoinRule::Private => Self::Private,
248            SpaceRoomJoinRule::Restricted => Self::Restricted,
249            SpaceRoomJoinRule::KnockRestricted => Self::KnockRestricted,
250            SpaceRoomJoinRule::Public => Self::Public,
251            SpaceRoomJoinRule::_Custom(custom) => Self::_Custom(custom),
252        }
253    }
254}
255
256/// An enum of possible room types to filter.
257///
258/// This type can hold an arbitrary string. To build this with a custom value, convert it from an
259/// `Option<string>` with `::from()` / `.into()`. [`RoomTypeFilter::Default`] can be constructed
260/// from `None`.
261///
262/// To check for values that are not available as a documented variant here, use its string
263/// representation, obtained through [`.as_str()`](Self::as_str()).
264#[derive(Clone, Debug, PartialEq, Eq)]
265#[non_exhaustive]
266pub enum RoomTypeFilter {
267    /// The default room type, defined without a `room_type`.
268    Default,
269
270    /// A space.
271    Space,
272
273    /// A custom room type.
274    #[doc(hidden)]
275    _Custom(PrivOwnedStr),
276}
277
278impl RoomTypeFilter {
279    /// Get the string representation of this `RoomTypeFilter`.
280    ///
281    /// [`RoomTypeFilter::Default`] returns `None`.
282    pub fn as_str(&self) -> Option<&str> {
283        match self {
284            RoomTypeFilter::Default => None,
285            RoomTypeFilter::Space => Some("m.space"),
286            RoomTypeFilter::_Custom(s) => Some(&s.0),
287        }
288    }
289}
290
291impl<T> From<Option<T>> for RoomTypeFilter
292where
293    T: AsRef<str> + Into<Box<str>>,
294{
295    fn from(s: Option<T>) -> Self {
296        match s {
297            None => Self::Default,
298            Some(s) => match s.as_ref() {
299                "m.space" => Self::Space,
300                _ => Self::_Custom(PrivOwnedStr(s.into())),
301            },
302        }
303    }
304}
305
306impl From<Option<RoomType>> for RoomTypeFilter {
307    fn from(t: Option<RoomType>) -> Self {
308        match t {
309            None => Self::Default,
310            Some(s) => match s {
311                RoomType::Space => Self::Space,
312                _ => Self::from(Some(s.as_str())),
313            },
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use assert_matches2::assert_matches;
321    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
322
323    use super::{Filter, RoomNetwork, RoomTypeFilter};
324    use crate::room::RoomType;
325
326    #[test]
327    fn test_from_room_type() {
328        let test = RoomType::Space;
329        let other: RoomTypeFilter = RoomTypeFilter::from(Some(test));
330        assert_eq!(other, RoomTypeFilter::Space);
331    }
332
333    #[test]
334    fn serialize_matrix_network_only() {
335        let json = json!({});
336        assert_eq!(to_json_value(RoomNetwork::Matrix).unwrap(), json);
337    }
338
339    #[test]
340    fn deserialize_matrix_network_only() {
341        let json = json!({ "include_all_networks": false });
342        assert_eq!(from_json_value::<RoomNetwork>(json).unwrap(), RoomNetwork::Matrix);
343    }
344
345    #[test]
346    fn serialize_default_network_is_empty() {
347        let json = json!({});
348        assert_eq!(to_json_value(RoomNetwork::default()).unwrap(), json);
349    }
350
351    #[test]
352    fn deserialize_empty_network_is_default() {
353        let json = json!({});
354        assert_eq!(from_json_value::<RoomNetwork>(json).unwrap(), RoomNetwork::Matrix);
355    }
356
357    #[test]
358    fn serialize_include_all_networks() {
359        let json = json!({ "include_all_networks": true });
360        assert_eq!(to_json_value(RoomNetwork::All).unwrap(), json);
361    }
362
363    #[test]
364    fn deserialize_include_all_networks() {
365        let json = json!({ "include_all_networks": true });
366        assert_eq!(from_json_value::<RoomNetwork>(json).unwrap(), RoomNetwork::All);
367    }
368
369    #[test]
370    fn serialize_third_party_network() {
371        let json = json!({ "third_party_instance_id": "freenode" });
372        assert_eq!(to_json_value(RoomNetwork::ThirdParty("freenode".to_owned())).unwrap(), json);
373    }
374
375    #[test]
376    fn deserialize_third_party_network() {
377        let json = json!({ "third_party_instance_id": "freenode" });
378        assert_eq!(
379            from_json_value::<RoomNetwork>(json).unwrap(),
380            RoomNetwork::ThirdParty("freenode".into())
381        );
382    }
383
384    #[test]
385    fn deserialize_include_all_networks_and_third_party_exclusivity() {
386        let json = json!({ "include_all_networks": true, "third_party_instance_id": "freenode" });
387        assert_eq!(
388            from_json_value::<RoomNetwork>(json).unwrap_err().to_string().as_str(),
389            "`include_all_networks = true` and `third_party_instance_id` are mutually exclusive."
390        );
391    }
392
393    #[test]
394    fn serialize_filter_empty() {
395        let filter = Filter::default();
396        let json = json!({});
397        assert_eq!(to_json_value(filter).unwrap(), json);
398    }
399
400    #[test]
401    fn deserialize_filter_empty() {
402        let json = json!({});
403        let filter = from_json_value::<Filter>(json).unwrap();
404        assert_eq!(filter.generic_search_term, None);
405        assert_eq!(filter.room_types.len(), 0);
406    }
407
408    #[test]
409    fn serialize_filter_room_types() {
410        let filter = Filter {
411            generic_search_term: None,
412            room_types: vec![
413                RoomTypeFilter::Default,
414                RoomTypeFilter::Space,
415                Some("custom_type").into(),
416            ],
417        };
418        let json = json!({ "room_types": [null, "m.space", "custom_type"] });
419        assert_eq!(to_json_value(filter).unwrap(), json);
420    }
421
422    #[test]
423    fn deserialize_filter_room_types() {
424        let json = json!({ "room_types": [null, "m.space", "custom_type"] });
425        let filter = from_json_value::<Filter>(json).unwrap();
426        assert_eq!(filter.room_types.len(), 3);
427        assert_eq!(filter.room_types[0], RoomTypeFilter::Default);
428        assert_eq!(filter.room_types[1], RoomTypeFilter::Space);
429        assert_matches!(&filter.room_types[2], RoomTypeFilter::_Custom(_));
430        assert_eq!(filter.room_types[2].as_str(), Some("custom_type"));
431    }
432}