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