matrix_sdk_base/room/
tombstone.rs

1// Copyright 2025 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::ops::Not;
16
17use ruma::{OwnedRoomId, events::room::tombstone::RoomTombstoneEventContent};
18
19use super::Room;
20
21impl Room {
22    /// Has the room been tombstoned.
23    ///
24    /// A room is tombstoned if it has received a [`m.room.tombstone`] state
25    /// event; see [`Room::tombstone_content`].
26    ///
27    /// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
28    pub fn is_tombstoned(&self) -> bool {
29        self.inner.read().base_info.tombstone.is_some()
30    }
31
32    /// Get the [`m.room.tombstone`] state event's content of this room if one
33    /// has been received.
34    ///
35    /// Also see [`Room::is_tombstoned`] to check if the [`m.room.tombstone`]
36    /// event has been received. It's faster than using this method.
37    ///
38    /// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
39    pub fn tombstone_content(&self) -> Option<RoomTombstoneEventContent> {
40        self.inner.read().tombstone().cloned()
41    }
42
43    /// If this room is tombstoned, return the “reference” to the successor room
44    /// —i.e. the room replacing this one.
45    ///
46    /// A room is tombstoned if it has received a [`m.room.tombstone`] state
47    /// event; see [`Room::tombstone_content`].
48    ///
49    /// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
50    pub fn successor_room(&self) -> Option<SuccessorRoom> {
51        self.tombstone_content().map(|tombstone_event| SuccessorRoom {
52            room_id: tombstone_event.replacement_room,
53            reason: tombstone_event.body.is_empty().not().then_some(tombstone_event.body),
54        })
55    }
56
57    /// If this room is the successor of a tombstoned room, return the
58    /// “reference” to the predecessor room.
59    ///
60    /// A room is tombstoned if it has received a [`m.room.tombstone`] state
61    /// event; see [`Room::tombstone_content`].
62    ///
63    /// To determine if a room is the successor of a tombstoned room, the
64    /// [`m.room.create`] must have been received, **with** a `predecessor`
65    /// field. See [`Room::create_content`].
66    ///
67    /// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
68    /// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
69    pub fn predecessor_room(&self) -> Option<PredecessorRoom> {
70        self.create_content()
71            .and_then(|content_event| content_event.predecessor)
72            .map(|predecessor| PredecessorRoom { room_id: predecessor.room_id })
73    }
74}
75
76/// When a room A is tombstoned, it is replaced by a room B. The room A is the
77/// predecessor of B, and B is the successor of A. This type holds information
78/// about the successor room. See [`Room::successor_room`].
79///
80/// A room is tombstoned if it has received a [`m.room.tombstone`] state event.
81///
82/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
83#[derive(Debug)]
84pub struct SuccessorRoom {
85    /// The ID of the next room replacing this (tombstoned) room.
86    pub room_id: OwnedRoomId,
87
88    /// The reason why the room has been tombstoned.
89    pub reason: Option<String>,
90}
91
92/// When a room A is tombstoned, it is replaced by a room B. The room A is the
93/// predecessor of B, and B is the successor of A. This type holds information
94/// about the predecessor room. See [`Room::predecessor_room`].
95///
96/// To know the predecessor of a room, the [`m.room.create`] state event must
97/// have been received.
98///
99/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
100#[derive(Debug)]
101pub struct PredecessorRoom {
102    /// The ID of the old room.
103    pub room_id: OwnedRoomId,
104}
105
106#[cfg(test)]
107mod tests {
108    use std::ops::Not;
109
110    use assert_matches::assert_matches;
111    use matrix_sdk_test::{
112        JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
113    };
114    use ruma::{RoomVersionId, room_id, user_id};
115
116    use crate::{RoomState, test_utils::logged_in_base_client};
117
118    #[async_test]
119    async fn test_no_successor_room() {
120        let client = logged_in_base_client(None).await;
121        let room = client.get_or_create_room(room_id!("!r0"), RoomState::Joined);
122
123        assert!(room.is_tombstoned().not());
124        assert!(room.tombstone_content().is_none());
125        assert!(room.successor_room().is_none());
126    }
127
128    #[async_test]
129    async fn test_successor_room() {
130        let client = logged_in_base_client(None).await;
131        let sender = user_id!("@mnt_io:matrix.org");
132        let room_id = room_id!("!r0");
133        let successor_room_id = room_id!("!r1");
134        let room = client.get_or_create_room(room_id, RoomState::Joined);
135
136        let mut sync_builder = SyncResponseBuilder::new();
137        let response = sync_builder
138            .add_joined_room(
139                JoinedRoomBuilder::new(room_id).add_timeline_event(
140                    EventFactory::new()
141                        .sender(sender)
142                        .room_tombstone("traces of you", successor_room_id),
143                ),
144            )
145            .build_sync_response();
146
147        client.receive_sync_response(response).await.unwrap();
148
149        assert!(room.is_tombstoned());
150        assert!(room.tombstone_content().is_some());
151        assert_matches!(room.successor_room(), Some(successor_room) => {
152            assert_eq!(successor_room.room_id, successor_room_id);
153            assert_matches!(successor_room.reason, Some(reason) => {
154                assert_eq!(reason, "traces of you");
155            });
156        });
157    }
158
159    #[async_test]
160    async fn test_successor_room_no_reason() {
161        let client = logged_in_base_client(None).await;
162        let sender = user_id!("@mnt_io:matrix.org");
163        let room_id = room_id!("!r0");
164        let successor_room_id = room_id!("!r1");
165        let room = client.get_or_create_room(room_id, RoomState::Joined);
166
167        let mut sync_builder = SyncResponseBuilder::new();
168        let response = sync_builder
169            .add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event(
170                EventFactory::new().sender(sender).room_tombstone(
171                    // An empty reason will result in `None` in `SuccessorRoom::reason`.
172                    "",
173                    successor_room_id,
174                ),
175            ))
176            .build_sync_response();
177
178        client.receive_sync_response(response).await.unwrap();
179
180        assert!(room.is_tombstoned());
181        assert!(room.tombstone_content().is_some());
182        assert_matches!(room.successor_room(), Some(successor_room) => {
183            assert_eq!(successor_room.room_id, successor_room_id);
184            assert!(successor_room.reason.is_none());
185        });
186    }
187
188    #[async_test]
189    async fn test_no_predecessor_room() {
190        let client = logged_in_base_client(None).await;
191        let room = client.get_or_create_room(room_id!("!r0"), RoomState::Joined);
192
193        assert!(room.create_content().is_none());
194        assert!(room.predecessor_room().is_none());
195    }
196
197    #[async_test]
198    async fn test_no_predecessor_room_with_create_event() {
199        let client = logged_in_base_client(None).await;
200        let sender = user_id!("@mnt_io:matrix.org");
201        let room_id = room_id!("!r1");
202        let room = client.get_or_create_room(room_id, RoomState::Joined);
203
204        let mut sync_builder = SyncResponseBuilder::new();
205        let response = sync_builder
206            .add_joined_room(
207                JoinedRoomBuilder::new(room_id).add_timeline_event(
208                    EventFactory::new()
209                        .create(sender, RoomVersionId::V11)
210                        // No `predecessor` field!
211                        .no_predecessor()
212                        .into_raw_sync(),
213                ),
214            )
215            .build_sync_response();
216
217        client.receive_sync_response(response).await.unwrap();
218
219        assert!(room.create_content().is_some());
220        assert!(room.predecessor_room().is_none());
221    }
222
223    #[async_test]
224    async fn test_predecessor_room() {
225        let client = logged_in_base_client(None).await;
226        let sender = user_id!("@mnt_io:matrix.org");
227        let room_id = room_id!("!r1");
228        let predecessor_room_id = room_id!("!r0");
229        let room = client.get_or_create_room(room_id, RoomState::Joined);
230
231        let mut sync_builder = SyncResponseBuilder::new();
232        let response = sync_builder
233            .add_joined_room(
234                JoinedRoomBuilder::new(room_id).add_timeline_event(
235                    EventFactory::new()
236                        .create(sender, RoomVersionId::V11)
237                        .predecessor(predecessor_room_id)
238                        .into_raw_sync(),
239                ),
240            )
241            .build_sync_response();
242
243        client.receive_sync_response(response).await.unwrap();
244
245        assert!(room.create_content().is_some());
246        assert_matches!(room.predecessor_room(), Some(predecessor_room) => {
247            assert_eq!(predecessor_room.room_id, predecessor_room_id);
248        });
249    }
250}