Skip to main content

matrix_sdk/room/
knock_requests.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::slice;
16
17use js_int::UInt;
18use ruma::{EventId, OwnedEventId, OwnedMxcUri, OwnedUserId, RoomId};
19
20use crate::{Error, Room, room::RoomMember};
21
22/// A request to join a room with `knock` join rule.
23#[derive(Debug, Clone)]
24pub struct KnockRequest {
25    room: Room,
26    /// The event id of the event containing knock membership change.
27    pub event_id: OwnedEventId,
28    /// The timestamp when this request was created.
29    pub timestamp: Option<UInt>,
30    /// Some general room member info to display.
31    pub member_info: KnockRequestMemberInfo,
32    /// Whether it's been marked as 'seen' by the client.
33    pub is_seen: bool,
34}
35
36impl KnockRequest {
37    pub(crate) fn new(
38        room: &Room,
39        event_id: &EventId,
40        timestamp: Option<UInt>,
41        member: KnockRequestMemberInfo,
42        is_seen: bool,
43    ) -> Self {
44        Self {
45            room: room.clone(),
46            event_id: event_id.to_owned(),
47            timestamp,
48            member_info: member,
49            is_seen,
50        }
51    }
52
53    /// The room id for the `Room` from whose access is requested.
54    pub fn room_id(&self) -> &RoomId {
55        self.room.room_id()
56    }
57
58    /// Marks the knock request as 'seen' so the client can ignore it in the
59    /// future.
60    pub async fn mark_as_seen(&self) -> Result<(), Error> {
61        self.room.mark_knock_requests_as_seen(slice::from_ref(&self.member_info.user_id)).await?;
62        Ok(())
63    }
64
65    /// Accepts the knock request by inviting the user to the room.
66    pub async fn accept(&self) -> Result<(), Error> {
67        self.room.invite_user_by_id(&self.member_info.user_id).await
68    }
69
70    /// Declines the knock request by kicking the user from the room, with an
71    /// optional reason.
72    pub async fn decline(&self, reason: Option<&str>) -> Result<(), Error> {
73        self.room.kick_user(&self.member_info.user_id, reason).await
74    }
75
76    /// Declines the knock request by banning the user from the room, with an
77    /// optional reason.
78    pub async fn decline_and_ban(&self, reason: Option<&str>) -> Result<(), Error> {
79        self.room.ban_user(&self.member_info.user_id, reason).await
80    }
81}
82
83/// General room member info to display along with the join request.
84#[derive(Debug, Clone)]
85pub struct KnockRequestMemberInfo {
86    /// The user id for the room member requesting access.
87    pub user_id: OwnedUserId,
88    /// The optional display name of the room member requesting access.
89    pub display_name: Option<String>,
90    /// The optional avatar url of the room member requesting access.
91    pub avatar_url: Option<OwnedMxcUri>,
92    /// An optional reason why the user wants access to the room.
93    pub reason: Option<String>,
94}
95
96impl KnockRequestMemberInfo {
97    pub(crate) fn from_member(member: &RoomMember) -> Self {
98        Self {
99            user_id: member.user_id().to_owned(),
100            display_name: member.display_name().map(ToOwned::to_owned),
101            avatar_url: member.avatar_url().map(ToOwned::to_owned),
102            reason: member.event().reason().map(ToOwned::to_owned),
103        }
104    }
105}
106
107// The http mocking library is not supported for wasm32
108#[cfg(all(test, not(target_family = "wasm")))]
109mod tests {
110    use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
111    use ruma::{
112        EventId, event_id, events::room::member::MembershipState, owned_user_id, room_id, user_id,
113    };
114
115    use crate::{
116        Room,
117        room::knock_requests::{KnockRequest, KnockRequestMemberInfo},
118        test_utils::mocks::MatrixMockServer,
119    };
120
121    #[async_test]
122    async fn test_mark_as_seen() {
123        let server = MatrixMockServer::new().await;
124        let client = server.client_builder().build().await;
125        let room_id = room_id!("!a:b.c");
126        let event_id = event_id!("$a:b.c");
127        let user_id = user_id!("@alice:b.c");
128
129        let f = EventFactory::new().room(room_id);
130        let joined_room_builder = JoinedRoomBuilder::new(room_id).add_state_bulk(vec![
131            f.member(user_id).membership(MembershipState::Knock).event_id(event_id).into(),
132        ]);
133        let room = server.sync_room(&client, joined_room_builder).await;
134
135        let knock_request = make_knock_request(&room, Some(event_id));
136
137        // When we mark the knock request as seen
138        knock_request.mark_as_seen().await.expect("Failed to mark as seen");
139
140        // Then we can check it was successfully marked as seen from the room
141        let seen_ids =
142            room.get_seen_knock_request_ids().await.expect("Failed to get seen join request ids");
143        assert_eq!(seen_ids.len(), 1);
144        assert_eq!(
145            seen_ids.into_iter().next().expect("Couldn't load next item"),
146            (event_id.to_owned(), user_id.to_owned())
147        );
148    }
149
150    #[async_test]
151    async fn test_accept() {
152        let server = MatrixMockServer::new().await;
153        let client = server.client_builder().build().await;
154        let room_id = room_id!("!a:b.c");
155
156        let room = server.sync_joined_room(&client, room_id).await;
157
158        let knock_request = make_knock_request(&room, None);
159
160        // The /invite endpoint must be called once
161        server.mock_invite_user_by_id().ok().mock_once().mount().await;
162
163        // When we accept the knock request
164        knock_request.accept().await.expect("Failed to accept the request");
165    }
166
167    #[async_test]
168    async fn test_decline() {
169        let server = MatrixMockServer::new().await;
170        let client = server.client_builder().build().await;
171        let room_id = room_id!("!a:b.c");
172
173        let room = server.sync_joined_room(&client, room_id).await;
174
175        let knock_request = make_knock_request(&room, None);
176
177        // The /kick endpoint must be called once
178        server.mock_kick_user().ok().mock_once().mount().await;
179
180        // When we decline the knock request
181        knock_request.decline(None).await.expect("Failed to decline the request");
182    }
183
184    #[async_test]
185    async fn test_decline_and_ban() {
186        let server = MatrixMockServer::new().await;
187        let client = server.client_builder().build().await;
188        let room_id = room_id!("!a:b.c");
189
190        let room = server.sync_joined_room(&client, room_id).await;
191
192        let knock_request = make_knock_request(&room, None);
193
194        // The /ban endpoint must be called once
195        server.mock_ban_user().ok().mock_once().mount().await;
196
197        // When we decline the knock request and ban the user from the room
198        knock_request
199            .decline_and_ban(None)
200            .await
201            .expect("Failed to decline the request and ban the user");
202    }
203
204    fn make_knock_request(room: &Room, event_id: Option<&EventId>) -> KnockRequest {
205        KnockRequest::new(
206            room,
207            event_id.unwrap_or(event_id!("$a:b.c")),
208            None,
209            KnockRequestMemberInfo {
210                user_id: owned_user_id!("@alice:b.c"),
211                display_name: None,
212                avatar_url: None,
213                reason: None,
214            },
215            false,
216        )
217    }
218}