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