Skip to main content

matrix_sdk/room/
privacy_settings.rs

1use matrix_sdk_base::Room as BaseRoom;
2use ruma::{
3    OwnedRoomAliasId, RoomAliasId,
4    api::client::{
5        directory::{get_room_visibility, set_room_visibility},
6        room::Visibility,
7        state::send_state_event,
8    },
9    assign,
10    events::{
11        EmptyStateKey,
12        room::{
13            canonical_alias::RoomCanonicalAliasEventContent,
14            history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
15            join_rules::{JoinRule, RoomJoinRulesEventContent},
16        },
17    },
18};
19
20use crate::{Client, Result};
21
22/// A helper to group the methods in [Room](crate::Room) related to the room's
23/// visibility and access.
24#[derive(Debug)]
25pub struct RoomPrivacySettings<'a> {
26    room: &'a BaseRoom,
27    client: &'a Client,
28}
29
30impl<'a> RoomPrivacySettings<'a> {
31    pub(crate) fn new(room: &'a BaseRoom, client: &'a Client) -> Self {
32        Self { room, client }
33    }
34
35    /// Publish a new room alias for this room in the room directory.
36    ///
37    /// Returns:
38    /// - `true` if the room alias didn't exist and it's now published.
39    /// - `false` if the room alias was already present so it couldn't be
40    ///   published.
41    pub async fn publish_room_alias_in_room_directory(
42        &'a self,
43        alias: &RoomAliasId,
44    ) -> Result<bool> {
45        if self.client.is_room_alias_available(alias).await? {
46            self.client.create_room_alias(alias, self.room.room_id()).await?;
47            return Ok(true);
48        }
49
50        Ok(false)
51    }
52
53    /// Remove an existing room alias for this room in the room directory.
54    ///
55    /// Returns:
56    /// - `true` if the room alias was present and it's now removed from the
57    ///   room directory.
58    /// - `false` if the room alias didn't exist so it couldn't be removed.
59    pub async fn remove_room_alias_from_room_directory(
60        &'a self,
61        alias: &RoomAliasId,
62    ) -> Result<bool> {
63        if self.client.resolve_room_alias(alias).await.is_ok() {
64            self.client.remove_room_alias(alias).await?;
65            return Ok(true);
66        }
67
68        Ok(false)
69    }
70
71    /// Update the canonical alias of the room.
72    ///
73    /// # Arguments:
74    /// * `alias` - The new main alias to use for the room. A `None` value
75    ///   removes the existing main canonical alias.
76    /// * `alt_aliases` - The list of alternative aliases for this room.
77    ///
78    /// See <https://spec.matrix.org/v1.12/client-server-api/#mroomcanonical_alias> for more info about the canonical alias.
79    ///
80    /// Note that publishing the alias in the room directory is done separately,
81    /// and a room alias must have already been published before it can be set
82    /// as the canonical alias.
83    pub async fn update_canonical_alias(
84        &'a self,
85        alias: Option<OwnedRoomAliasId>,
86        alt_aliases: Vec<OwnedRoomAliasId>,
87    ) -> Result<()> {
88        // Create a new alias event combining both the new and previous values
89        let content = assign!(
90            RoomCanonicalAliasEventContent::new(),
91            { alias, alt_aliases }
92        );
93
94        // Send the state event
95        let request = send_state_event::v3::Request::new(
96            self.room.room_id().to_owned(),
97            &EmptyStateKey,
98            &content,
99        )?;
100        self.client.send(request).await?;
101
102        Ok(())
103    }
104
105    /// Update room history visibility for this room.
106    ///
107    /// The history visibility controls whether a user can see the events that
108    /// happened in a room before they joined.
109    ///
110    /// See <https://spec.matrix.org/v1.12/client-server-api/#mroomcanonical_alias> for more info.
111    pub async fn update_room_history_visibility(
112        &'a self,
113        new_value: HistoryVisibility,
114    ) -> Result<()> {
115        let request = send_state_event::v3::Request::new(
116            self.room.room_id().to_owned(),
117            &EmptyStateKey,
118            &RoomHistoryVisibilityEventContent::new(new_value),
119        )?;
120        self.client.send(request).await?;
121        Ok(())
122    }
123
124    /// Update the join rule for this room.
125    ///
126    /// The join rules controls if and how a new user can get access to the
127    /// room.
128    ///
129    /// See <https://spec.matrix.org/v1.12/client-server-api/#mroomjoin_rules> for more info.
130    pub async fn update_join_rule(&'a self, new_rule: JoinRule) -> Result<()> {
131        let request = send_state_event::v3::Request::new(
132            self.room.room_id().to_owned(),
133            &EmptyStateKey,
134            &RoomJoinRulesEventContent::new(new_rule),
135        )?;
136        self.client.send(request).await?;
137        Ok(())
138    }
139
140    /// Returns the visibility for this room in the room directory.
141    ///
142    /// [Public](`Visibility::Public`) rooms are listed in the room directory
143    /// and can be found using it.
144    pub async fn get_room_visibility(&'a self) -> Result<Visibility> {
145        let request = get_room_visibility::v3::Request::new(self.room.room_id().to_owned());
146        let response = self.client.send(request).await?;
147        Ok(response.visibility)
148    }
149
150    /// Update the visibility for this room in the room directory.
151    ///
152    /// [Public](`Visibility::Public`) rooms are listed in the room directory
153    /// and can be found using it.
154    pub async fn update_room_visibility(&'a self, visibility: Visibility) -> Result<()> {
155        let request =
156            set_room_visibility::v3::Request::new(self.room.room_id().to_owned(), visibility);
157
158        self.client.send(request).await?;
159
160        Ok(())
161    }
162}
163
164#[cfg(all(test, not(target_family = "wasm")))]
165mod tests {
166    use std::ops::Not;
167
168    use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
169    use ruma::{
170        api::client::room::Visibility,
171        event_id,
172        events::{
173            StateEventType,
174            room::{history_visibility::HistoryVisibility, join_rules::JoinRule},
175        },
176        owned_room_alias_id, room_id, user_id,
177    };
178
179    use crate::test_utils::mocks::MatrixMockServer;
180
181    #[async_test]
182    async fn test_publish_room_alias_to_room_directory() {
183        let server = MatrixMockServer::new().await;
184        let client = server.client_builder().build().await;
185
186        let room_id = room_id!("!a:b.c");
187        let room = server.sync_joined_room(&client, room_id).await;
188
189        let room_alias = owned_room_alias_id!("#a:b.c");
190
191        // First we'd check if the new alias needs to be created
192        server
193            .mock_room_directory_resolve_alias()
194            .for_alias(room_alias.to_string())
195            .not_found()
196            .mock_once()
197            .mount()
198            .await;
199
200        // After that, we'd create a new room alias association in the room directory
201        server.mock_room_directory_create_room_alias().ok().mock_once().mount().await;
202
203        let published = room
204            .privacy_settings()
205            .publish_room_alias_in_room_directory(&room_alias)
206            .await
207            .expect("we should get a result value, not an error");
208        assert!(published);
209    }
210
211    #[async_test]
212    async fn test_publish_room_alias_to_room_directory_when_alias_exists() {
213        let server = MatrixMockServer::new().await;
214        let client = server.client_builder().build().await;
215
216        let room_id = room_id!("!a:b.c");
217        let room = server.sync_joined_room(&client, room_id).await;
218
219        let room_alias = owned_room_alias_id!("#a:b.c");
220
221        // First we'd check if the new alias needs to be created. It does not.
222        server
223            .mock_room_directory_resolve_alias()
224            .for_alias(room_alias.to_string())
225            .ok(room_id.as_ref(), Vec::new())
226            .mock_once()
227            .mount()
228            .await;
229
230        // Since the room alias already exists we won't create it again.
231        server.mock_room_directory_create_room_alias().ok().never().mount().await;
232
233        let published = room
234            .privacy_settings()
235            .publish_room_alias_in_room_directory(&room_alias)
236            .await
237            .expect("we should get a result value, not an error");
238        assert!(published.not());
239    }
240
241    #[async_test]
242    async fn test_remove_room_alias() {
243        let server = MatrixMockServer::new().await;
244        let client = server.client_builder().build().await;
245
246        let room_id = room_id!("!a:b.c");
247        let f = EventFactory::new().sender(user_id!("@example:localhost"));
248        let joined_room_builder = JoinedRoomBuilder::new(room_id).add_state_event(
249            f.canonical_alias(Some(owned_room_alias_id!("#tutorial:localhost")), vec![]),
250        );
251        let room = server.sync_room(&client, joined_room_builder).await;
252
253        let room_alias = owned_room_alias_id!("#a:b.c");
254
255        // First we'd check if the alias exists
256        server
257            .mock_room_directory_resolve_alias()
258            .for_alias(room_alias.to_string())
259            .ok(room_id.as_ref(), Vec::new())
260            .mock_once()
261            .mount()
262            .await;
263
264        // After that we'd remove it
265        server.mock_room_directory_remove_room_alias().ok().mock_once().mount().await;
266
267        let removed = room
268            .privacy_settings()
269            .remove_room_alias_from_room_directory(&room_alias)
270            .await
271            .expect("we should get a result value, not an error");
272        assert!(removed);
273    }
274
275    #[async_test]
276    async fn test_remove_room_alias_if_it_does_not_exist() {
277        let server = MatrixMockServer::new().await;
278        let client = server.client_builder().build().await;
279
280        let room_id = room_id!("!a:b.c");
281        let f = EventFactory::new().sender(user_id!("@example:localhost"));
282        let joined_room_builder = JoinedRoomBuilder::new(room_id).add_state_event(
283            f.canonical_alias(Some(owned_room_alias_id!("#tutorial:localhost")), vec![]),
284        );
285        let room = server.sync_room(&client, joined_room_builder).await;
286
287        let room_alias = owned_room_alias_id!("#a:b.c");
288
289        // First we'd check if the alias exists. It doesn't.
290        server
291            .mock_room_directory_resolve_alias()
292            .for_alias(room_alias.to_string())
293            .not_found()
294            .mock_once()
295            .mount()
296            .await;
297
298        // So we can't remove it after the check.
299        server.mock_room_directory_remove_room_alias().ok().never().mount().await;
300
301        let removed = room
302            .privacy_settings()
303            .remove_room_alias_from_room_directory(&room_alias)
304            .await
305            .expect("we should get a result value, not an error");
306        assert!(removed.not());
307    }
308
309    #[async_test]
310    async fn test_update_canonical_alias_with_some_value() {
311        let server = MatrixMockServer::new().await;
312        let client = server.client_builder().build().await;
313
314        let room_id = room_id!("!a:b.c");
315        let room = server.sync_joined_room(&client, room_id).await;
316
317        server
318            .mock_room_send_state()
319            .for_type(StateEventType::RoomCanonicalAlias)
320            .ok(event_id!("$a:b.c"))
321            .mock_once()
322            .mount()
323            .await;
324
325        let room_alias = owned_room_alias_id!("#a:b.c");
326        let ret = room
327            .privacy_settings()
328            .update_canonical_alias(Some(room_alias.clone()), Vec::new())
329            .await;
330        assert!(ret.is_ok());
331    }
332
333    #[async_test]
334    async fn test_update_canonical_alias_with_no_value() {
335        let server = MatrixMockServer::new().await;
336        let client = server.client_builder().build().await;
337
338        let room_id = room_id!("!a:b.c");
339        let room = server.sync_joined_room(&client, room_id).await;
340
341        server
342            .mock_room_send_state()
343            .for_type(StateEventType::RoomCanonicalAlias)
344            .ok(event_id!("$a:b.c"))
345            .mock_once()
346            .mount()
347            .await;
348
349        let ret = room.privacy_settings().update_canonical_alias(None, Vec::new()).await;
350        assert!(ret.is_ok());
351    }
352
353    #[async_test]
354    async fn test_update_room_history_visibility() {
355        let server = MatrixMockServer::new().await;
356        let client = server.client_builder().build().await;
357
358        let room_id = room_id!("!a:b.c");
359        let room = server.sync_joined_room(&client, room_id).await;
360
361        server
362            .mock_room_send_state()
363            .for_type(StateEventType::RoomHistoryVisibility)
364            .ok(event_id!("$a:b.c"))
365            .mock_once()
366            .mount()
367            .await;
368
369        let ret =
370            room.privacy_settings().update_room_history_visibility(HistoryVisibility::Joined).await;
371        assert!(ret.is_ok());
372    }
373
374    #[async_test]
375    async fn test_update_join_rule() {
376        let server = MatrixMockServer::new().await;
377        let client = server.client_builder().build().await;
378
379        let room_id = room_id!("!a:b.c");
380        let room = server.sync_joined_room(&client, room_id).await;
381
382        server
383            .mock_room_send_state()
384            .for_type(StateEventType::RoomJoinRules)
385            .ok(event_id!("$a:b.c"))
386            .mock_once()
387            .mount()
388            .await;
389
390        let ret = room.privacy_settings().update_join_rule(JoinRule::Public).await;
391        assert!(ret.is_ok());
392    }
393
394    #[async_test]
395    async fn test_get_room_visibility() {
396        let server = MatrixMockServer::new().await;
397        let client = server.client_builder().build().await;
398
399        let room_id = room_id!("!a:b.c");
400        let room = server.sync_joined_room(&client, room_id).await;
401
402        server
403            .mock_room_send_state()
404            .for_type(StateEventType::RoomJoinRules)
405            .ok(event_id!("$a:b.c"))
406            .mock_once()
407            .mount()
408            .await;
409
410        let ret = room.privacy_settings().update_join_rule(JoinRule::Public).await;
411        assert!(ret.is_ok());
412    }
413
414    #[async_test]
415    async fn test_update_room_visibility() {
416        let server = MatrixMockServer::new().await;
417        let client = server.client_builder().build().await;
418
419        let room_id = room_id!("!a:b.c");
420        let room = server.sync_joined_room(&client, room_id).await;
421
422        server.mock_room_directory_set_room_visibility().ok().mock_once().mount().await;
423
424        let ret = room.privacy_settings().update_room_visibility(Visibility::Private).await;
425        assert!(ret.is_ok());
426    }
427}