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