matrix_sdk/room/
privacy_settings.rs

1use matrix_sdk_base::Room as BaseRoom;
2use ruma::{
3    api::client::{
4        directory::{get_room_visibility, set_room_visibility},
5        room::Visibility,
6        state::send_state_event,
7    },
8    assign,
9    events::{
10        room::{
11            canonical_alias::RoomCanonicalAliasEventContent,
12            history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
13            join_rules::{JoinRule, RoomJoinRulesEventContent},
14        },
15        EmptyStateKey,
16    },
17    OwnedRoomAliasId, RoomAliasId,
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_arch = "wasm32")))]
165mod tests {
166    use std::ops::Not;
167
168    use matrix_sdk_test::{async_test, JoinedRoomBuilder, StateTestEvent};
169    use ruma::{
170        api::client::room::Visibility,
171        event_id,
172        events::{
173            room::{history_visibility::HistoryVisibility, join_rules::JoinRule},
174            StateEventType,
175        },
176        owned_room_alias_id, room_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 joined_room_builder =
248            JoinedRoomBuilder::new(room_id).add_state_event(StateTestEvent::Alias);
249        let room = server.sync_room(&client, joined_room_builder).await;
250
251        let room_alias = owned_room_alias_id!("#a:b.c");
252
253        // First we'd check if the alias exists
254        server
255            .mock_room_directory_resolve_alias()
256            .for_alias(room_alias.to_string())
257            .ok(room_id.as_ref(), Vec::new())
258            .mock_once()
259            .mount()
260            .await;
261
262        // After that we'd remove it
263        server.mock_room_directory_remove_room_alias().ok().mock_once().mount().await;
264
265        let removed = room
266            .privacy_settings()
267            .remove_room_alias_from_room_directory(&room_alias)
268            .await
269            .expect("we should get a result value, not an error");
270        assert!(removed);
271    }
272
273    #[async_test]
274    async fn test_remove_room_alias_if_it_does_not_exist() {
275        let server = MatrixMockServer::new().await;
276        let client = server.client_builder().build().await;
277
278        let room_id = room_id!("!a:b.c");
279        let joined_room_builder =
280            JoinedRoomBuilder::new(room_id).add_state_event(StateTestEvent::Alias);
281        let room = server.sync_room(&client, joined_room_builder).await;
282
283        let room_alias = owned_room_alias_id!("#a:b.c");
284
285        // First we'd check if the alias exists. It doesn't.
286        server
287            .mock_room_directory_resolve_alias()
288            .for_alias(room_alias.to_string())
289            .not_found()
290            .mock_once()
291            .mount()
292            .await;
293
294        // So we can't remove it after the check.
295        server.mock_room_directory_remove_room_alias().ok().never().mount().await;
296
297        let removed = room
298            .privacy_settings()
299            .remove_room_alias_from_room_directory(&room_alias)
300            .await
301            .expect("we should get a result value, not an error");
302        assert!(removed.not());
303    }
304
305    #[async_test]
306    async fn test_update_canonical_alias_with_some_value() {
307        let server = MatrixMockServer::new().await;
308        let client = server.client_builder().build().await;
309
310        let room_id = room_id!("!a:b.c");
311        let room = server.sync_joined_room(&client, room_id).await;
312
313        server
314            .mock_room_send_state()
315            .for_type(StateEventType::RoomCanonicalAlias)
316            .ok(event_id!("$a:b.c"))
317            .mock_once()
318            .mount()
319            .await;
320
321        let room_alias = owned_room_alias_id!("#a:b.c");
322        let ret = room
323            .privacy_settings()
324            .update_canonical_alias(Some(room_alias.clone()), Vec::new())
325            .await;
326        assert!(ret.is_ok());
327    }
328
329    #[async_test]
330    async fn test_update_canonical_alias_with_no_value() {
331        let server = MatrixMockServer::new().await;
332        let client = server.client_builder().build().await;
333
334        let room_id = room_id!("!a:b.c");
335        let room = server.sync_joined_room(&client, room_id).await;
336
337        server
338            .mock_room_send_state()
339            .for_type(StateEventType::RoomCanonicalAlias)
340            .ok(event_id!("$a:b.c"))
341            .mock_once()
342            .mount()
343            .await;
344
345        let ret = room.privacy_settings().update_canonical_alias(None, Vec::new()).await;
346        assert!(ret.is_ok());
347    }
348
349    #[async_test]
350    async fn test_update_room_history_visibility() {
351        let server = MatrixMockServer::new().await;
352        let client = server.client_builder().build().await;
353
354        let room_id = room_id!("!a:b.c");
355        let room = server.sync_joined_room(&client, room_id).await;
356
357        server
358            .mock_room_send_state()
359            .for_type(StateEventType::RoomHistoryVisibility)
360            .ok(event_id!("$a:b.c"))
361            .mock_once()
362            .mount()
363            .await;
364
365        let ret =
366            room.privacy_settings().update_room_history_visibility(HistoryVisibility::Joined).await;
367        assert!(ret.is_ok());
368    }
369
370    #[async_test]
371    async fn test_update_join_rule() {
372        let server = MatrixMockServer::new().await;
373        let client = server.client_builder().build().await;
374
375        let room_id = room_id!("!a:b.c");
376        let room = server.sync_joined_room(&client, room_id).await;
377
378        server
379            .mock_room_send_state()
380            .for_type(StateEventType::RoomJoinRules)
381            .ok(event_id!("$a:b.c"))
382            .mock_once()
383            .mount()
384            .await;
385
386        let ret = room.privacy_settings().update_join_rule(JoinRule::Public).await;
387        assert!(ret.is_ok());
388    }
389
390    #[async_test]
391    async fn test_get_room_visibility() {
392        let server = MatrixMockServer::new().await;
393        let client = server.client_builder().build().await;
394
395        let room_id = room_id!("!a:b.c");
396        let room = server.sync_joined_room(&client, room_id).await;
397
398        server
399            .mock_room_send_state()
400            .for_type(StateEventType::RoomJoinRules)
401            .ok(event_id!("$a:b.c"))
402            .mock_once()
403            .mount()
404            .await;
405
406        let ret = room.privacy_settings().update_join_rule(JoinRule::Public).await;
407        assert!(ret.is_ok());
408    }
409
410    #[async_test]
411    async fn test_update_room_visibility() {
412        let server = MatrixMockServer::new().await;
413        let client = server.client_builder().build().await;
414
415        let room_id = room_id!("!a:b.c");
416        let room = server.sync_joined_room(&client, room_id).await;
417
418        server.mock_room_directory_set_room_visibility().ok().mock_once().mount().await;
419
420        let ret = room.privacy_settings().update_room_visibility(Visibility::Private).await;
421        assert!(ret.is_ok());
422    }
423}