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