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