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::{events::room::tombstone::RoomTombstoneEventContent, OwnedEventId, OwnedRoomId};
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().and_then(|content_event| content_event.predecessor).map(
71            |predecessor| PredecessorRoom {
72                room_id: predecessor.room_id,
73                last_event_id: predecessor.event_id,
74            },
75        )
76    }
77}
78
79/// When a room A is tombstoned, it is replaced by a room B. The room A is the
80/// predecessor of B, and B is the successor of A. This type holds information
81/// about the successor room. See [`Room::successor_room`].
82///
83/// A room is tombstoned if it has received a [`m.room.tombstone`] state event.
84///
85/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
86#[derive(Debug)]
87pub struct SuccessorRoom {
88    /// The ID of the next room replacing this (tombstoned) room.
89    pub room_id: OwnedRoomId,
90
91    /// The reason why the room has been tombstoned.
92    pub reason: Option<String>,
93}
94
95/// When a room A is tombstoned, it is replaced by a room B. The room A is the
96/// predecessor of B, and B is the successor of A. This type holds information
97/// about the predecessor room. See [`Room::predecessor_room`].
98///
99/// To know the predecessor of a room, the [`m.room.create`] state event must
100/// have been received.
101///
102/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
103#[derive(Debug)]
104pub struct PredecessorRoom {
105    /// The ID of the old room.
106    pub room_id: OwnedRoomId,
107
108    /// The event ID of the last known event in the predecesssor room.
109    pub last_event_id: OwnedEventId,
110}
111
112#[cfg(test)]
113mod tests {
114    use std::ops::Not;
115
116    use assert_matches::assert_matches;
117    use matrix_sdk_test::{
118        async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
119    };
120    use ruma::{event_id, room_id, user_id, RoomVersionId};
121
122    use crate::{test_utils::logged_in_base_client, RoomState};
123
124    #[async_test]
125    async fn test_no_successor_room() {
126        let client = logged_in_base_client(None).await;
127        let room = client.get_or_create_room(room_id!("!r0"), RoomState::Joined);
128
129        assert!(room.is_tombstoned().not());
130        assert!(room.tombstone_content().is_none());
131        assert!(room.successor_room().is_none());
132    }
133
134    #[async_test]
135    async fn test_successor_room() {
136        let client = logged_in_base_client(None).await;
137        let sender = user_id!("@mnt_io:matrix.org");
138        let room_id = room_id!("!r0");
139        let successor_room_id = room_id!("!r1");
140        let room = client.get_or_create_room(room_id, RoomState::Joined);
141
142        let mut sync_builder = SyncResponseBuilder::new();
143        let response = sync_builder
144            .add_joined_room(
145                JoinedRoomBuilder::new(room_id).add_timeline_event(
146                    EventFactory::new()
147                        .sender(sender)
148                        .room_tombstone("traces of you", successor_room_id),
149                ),
150            )
151            .build_sync_response();
152
153        client.receive_sync_response(response).await.unwrap();
154
155        assert!(room.is_tombstoned());
156        assert!(room.tombstone_content().is_some());
157        assert_matches!(room.successor_room(), Some(successor_room) => {
158            assert_eq!(successor_room.room_id, successor_room_id);
159            assert_matches!(successor_room.reason, Some(reason) => {
160                assert_eq!(reason, "traces of you");
161            });
162        });
163    }
164
165    #[async_test]
166    async fn test_successor_room_no_reason() {
167        let client = logged_in_base_client(None).await;
168        let sender = user_id!("@mnt_io:matrix.org");
169        let room_id = room_id!("!r0");
170        let successor_room_id = room_id!("!r1");
171        let room = client.get_or_create_room(room_id, RoomState::Joined);
172
173        let mut sync_builder = SyncResponseBuilder::new();
174        let response = sync_builder
175            .add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event(
176                EventFactory::new().sender(sender).room_tombstone(
177                    // An empty reason will result in `None` in `SuccessorRoom::reason`.
178                    "",
179                    successor_room_id,
180                ),
181            ))
182            .build_sync_response();
183
184        client.receive_sync_response(response).await.unwrap();
185
186        assert!(room.is_tombstoned());
187        assert!(room.tombstone_content().is_some());
188        assert_matches!(room.successor_room(), Some(successor_room) => {
189            assert_eq!(successor_room.room_id, successor_room_id);
190            assert!(successor_room.reason.is_none());
191        });
192    }
193
194    #[async_test]
195    async fn test_no_predecessor_room() {
196        let client = logged_in_base_client(None).await;
197        let room = client.get_or_create_room(room_id!("!r0"), RoomState::Joined);
198
199        assert!(room.create_content().is_none());
200        assert!(room.predecessor_room().is_none());
201    }
202
203    #[async_test]
204    async fn test_no_predecessor_room_with_create_event() {
205        let client = logged_in_base_client(None).await;
206        let sender = user_id!("@mnt_io:matrix.org");
207        let room_id = room_id!("!r1");
208        let room = client.get_or_create_room(room_id, RoomState::Joined);
209
210        let mut sync_builder = SyncResponseBuilder::new();
211        let response = sync_builder
212            .add_joined_room(
213                JoinedRoomBuilder::new(room_id).add_timeline_event(
214                    EventFactory::new()
215                        .create(sender, RoomVersionId::V11)
216                        // No `predecessor` field!
217                        .no_predecessor()
218                        .into_raw_sync(),
219                ),
220            )
221            .build_sync_response();
222
223        client.receive_sync_response(response).await.unwrap();
224
225        assert!(room.create_content().is_some());
226        assert!(room.predecessor_room().is_none());
227    }
228
229    #[async_test]
230    async fn test_predecessor_room() {
231        let client = logged_in_base_client(None).await;
232        let sender = user_id!("@mnt_io:matrix.org");
233        let room_id = room_id!("!r1");
234        let predecessor_room_id = room_id!("!r0");
235        let predecessor_last_event_id = event_id!("$ev42");
236        let room = client.get_or_create_room(room_id, RoomState::Joined);
237
238        let mut sync_builder = SyncResponseBuilder::new();
239        let response = sync_builder
240            .add_joined_room(
241                JoinedRoomBuilder::new(room_id).add_timeline_event(
242                    EventFactory::new()
243                        .create(sender, RoomVersionId::V11)
244                        .predecessor(predecessor_room_id, predecessor_last_event_id)
245                        .into_raw_sync(),
246                ),
247            )
248            .build_sync_response();
249
250        client.receive_sync_response(response).await.unwrap();
251
252        assert!(room.create_content().is_some());
253        assert_matches!(room.predecessor_room(), Some(predecessor_room) => {
254            assert_eq!(predecessor_room.room_id, predecessor_room_id);
255            assert_eq!(predecessor_room.last_event_id, predecessor_last_event_id);
256        });
257    }
258}