matrix_sdk_base/store/
migration_helpers.rs

1// Copyright 2023 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
15//! Data migration helpers for StateStore implementations.
16
17use std::{
18    collections::{BTreeMap, HashSet},
19    sync::Arc,
20};
21
22use matrix_sdk_common::deserialized_responses::TimelineEvent;
23use ruma::{
24    events::{
25        direct::OwnedDirectUserIdentifier,
26        room::{
27            avatar::RoomAvatarEventContent,
28            canonical_alias::RoomCanonicalAliasEventContent,
29            create::RoomCreateEventContent,
30            encryption::RoomEncryptionEventContent,
31            guest_access::RoomGuestAccessEventContent,
32            history_visibility::RoomHistoryVisibilityEventContent,
33            join_rules::RoomJoinRulesEventContent,
34            name::{RedactedRoomNameEventContent, RoomNameEventContent},
35            tombstone::RoomTombstoneEventContent,
36            topic::RoomTopicEventContent,
37        },
38        EmptyStateKey, EventContent, RedactContent, StateEventContent, StateEventType,
39    },
40    OwnedRoomId, OwnedUserId, RoomId,
41};
42use serde::{Deserialize, Serialize};
43
44use crate::{
45    deserialized_responses::SyncOrStrippedState,
46    latest_event::LatestEvent,
47    room::{BaseRoomInfo, RoomSummary, SyncInfo},
48    sync::UnreadNotificationsCount,
49    MinimalStateEvent, OriginalMinimalStateEvent, RoomInfo, RoomState,
50};
51
52/// [`RoomInfo`] version 1.
53///
54/// The `name` field in `RoomNameEventContent` was optional and has become
55/// required. It means that sometimes the field has been serialized with the
56/// value `null`.
57///
58/// For the migration:
59///
60/// 1. Deserialize the stored room info using this type,
61/// 2. Get the `m.room.create` event for the room, if it is available,
62/// 3. Convert this to [`RoomInfo`] with `.migrate(create_event)`,
63/// 4. Replace the room info in the store.
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct RoomInfoV1 {
66    room_id: OwnedRoomId,
67    room_type: RoomState,
68    notification_counts: UnreadNotificationsCount,
69    summary: RoomSummary,
70    members_synced: bool,
71    last_prev_batch: Option<String>,
72    #[serde(default = "sync_info_complete")] // see fn docs for why we use this default
73    sync_info: SyncInfo,
74    #[serde(default = "encryption_state_default")] // see fn docs for why we use this default
75    encryption_state_synced: bool,
76    latest_event: Option<TimelineEvent>,
77    base_info: BaseRoomInfoV1,
78}
79
80impl RoomInfoV1 {
81    /// Get the room ID of this room.
82    pub fn room_id(&self) -> &RoomId {
83        &self.room_id
84    }
85
86    /// Returns the state this room is in.
87    pub fn state(&self) -> RoomState {
88        self.room_type
89    }
90
91    /// Migrate this to a [`RoomInfo`], using the given `m.room.create` event
92    /// from the room state.
93    pub fn migrate(self, create: Option<&SyncOrStrippedState<RoomCreateEventContent>>) -> RoomInfo {
94        let RoomInfoV1 {
95            room_id,
96            room_type,
97            notification_counts,
98            summary,
99            members_synced,
100            last_prev_batch,
101            sync_info,
102            encryption_state_synced,
103            latest_event,
104            base_info,
105        } = self;
106
107        RoomInfo {
108            data_format_version: 0,
109            room_id,
110            room_state: room_type,
111            notification_counts,
112            summary,
113            members_synced,
114            last_prev_batch,
115            sync_info,
116            encryption_state_synced,
117            latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))),
118            read_receipts: Default::default(),
119            base_info: base_info.migrate(create),
120            warned_about_unknown_room_version: Arc::new(false.into()),
121            cached_display_name: None,
122            cached_user_defined_notification_mode: None,
123            recency_stamp: None,
124        }
125    }
126}
127
128// The sync_info field introduced a new field in the database schema, for
129// backwards compatibility we assume that if the room is in the database, yet
130// the field isn't, we have synced it before this field was introduced - which
131// was a a full sync.
132fn sync_info_complete() -> SyncInfo {
133    SyncInfo::FullySynced
134}
135
136// The encryption_state_synced field introduced a new field in the database
137// schema, for backwards compatibility we assume that if the room is in the
138// database, yet the field isn't, we have synced it before this field was
139// introduced - which was a a full sync.
140fn encryption_state_default() -> bool {
141    true
142}
143
144/// [`BaseRoomInfo`] version 1.
145#[derive(Clone, Debug, Serialize, Deserialize)]
146struct BaseRoomInfoV1 {
147    avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
148    canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
149    dm_targets: HashSet<OwnedUserId>,
150    encryption: Option<RoomEncryptionEventContent>,
151    guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
152    history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
153    join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
154    max_power_level: i64,
155    name: Option<MinimalStateEvent<RoomNameEventContentV1>>,
156    tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
157    topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
158}
159
160impl BaseRoomInfoV1 {
161    /// Migrate this to a [`BaseRoomInfo`].
162    fn migrate(
163        self,
164        create: Option<&SyncOrStrippedState<RoomCreateEventContent>>,
165    ) -> Box<BaseRoomInfo> {
166        let BaseRoomInfoV1 {
167            avatar,
168            canonical_alias,
169            dm_targets,
170            encryption,
171            guest_access,
172            history_visibility,
173            join_rules,
174            max_power_level,
175            name,
176            tombstone,
177            topic,
178        } = self;
179
180        let create = create.map(|ev| match ev {
181            SyncOrStrippedState::Sync(e) => e.into(),
182            SyncOrStrippedState::Stripped(e) => e.into(),
183        });
184        let name = name.map(|name| match name {
185            MinimalStateEvent::Original(ev) => {
186                MinimalStateEvent::Original(OriginalMinimalStateEvent {
187                    content: ev.content.into(),
188                    event_id: ev.event_id,
189                })
190            }
191            MinimalStateEvent::Redacted(ev) => MinimalStateEvent::Redacted(ev),
192        });
193
194        let mut converted_dm_targets = HashSet::new();
195        for dm_target in dm_targets {
196            converted_dm_targets.insert(OwnedDirectUserIdentifier::from(dm_target));
197        }
198
199        Box::new(BaseRoomInfo {
200            avatar,
201            beacons: BTreeMap::new(),
202            canonical_alias,
203            create,
204            dm_targets: converted_dm_targets,
205            encryption,
206            guest_access,
207            history_visibility,
208            join_rules,
209            max_power_level,
210            name,
211            tombstone,
212            topic,
213            ..Default::default()
214        })
215    }
216}
217
218/// [`RoomNameEventContent`] version 1, with an optional `name`.
219#[derive(Clone, Debug, Serialize, Deserialize)]
220struct RoomNameEventContentV1 {
221    name: Option<String>,
222}
223
224impl EventContent for RoomNameEventContentV1 {
225    type EventType = StateEventType;
226
227    fn event_type(&self) -> Self::EventType {
228        StateEventType::RoomName
229    }
230}
231
232impl StateEventContent for RoomNameEventContentV1 {
233    type StateKey = EmptyStateKey;
234}
235
236impl RedactContent for RoomNameEventContentV1 {
237    type Redacted = RedactedRoomNameEventContent;
238
239    fn redact(self, _version: &ruma::RoomVersionId) -> Self::Redacted {
240        RedactedRoomNameEventContent::new()
241    }
242}
243
244impl From<RoomNameEventContentV1> for RoomNameEventContent {
245    fn from(value: RoomNameEventContentV1) -> Self {
246        RoomNameEventContent::new(value.name.unwrap_or_default())
247    }
248}