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::{collections::HashSet, sync::Arc};
18
19use matrix_sdk_common::deserialized_responses::TimelineEvent;
20use ruma::{
21    OwnedRoomId, OwnedUserId, RoomId,
22    events::{
23        direct::OwnedDirectUserIdentifier,
24        room::{
25            avatar::PossiblyRedactedRoomAvatarEventContent,
26            canonical_alias::PossiblyRedactedRoomCanonicalAliasEventContent,
27            create::RoomCreateEventContent, encryption::RoomEncryptionEventContent,
28            guest_access::PossiblyRedactedRoomGuestAccessEventContent,
29            history_visibility::PossiblyRedactedRoomHistoryVisibilityEventContent,
30            join_rules::PossiblyRedactedRoomJoinRulesEventContent,
31            name::PossiblyRedactedRoomNameEventContent,
32            tombstone::PossiblyRedactedRoomTombstoneEventContent,
33            topic::PossiblyRedactedRoomTopicEventContent,
34        },
35    },
36};
37use serde::{Deserialize, Serialize};
38
39use crate::{
40    MinimalStateEvent, RoomInfo, RoomState,
41    deserialized_responses::SyncOrStrippedState,
42    latest_event::LatestEventValue,
43    room::{BaseRoomInfo, RoomSummary, SyncInfo},
44    sync::UnreadNotificationsCount,
45};
46
47/// [`RoomInfo`] version 1.
48///
49/// The `name` field in `RoomNameEventContent` was optional and has become
50/// required. It means that sometimes the field has been serialized with the
51/// value `null`.
52///
53/// For the migration:
54///
55/// 1. Deserialize the stored room info using this type,
56/// 2. Get the `m.room.create` event for the room, if it is available,
57/// 3. Convert this to [`RoomInfo`] with `.migrate(create_event)`,
58/// 4. Replace the room info in the store.
59#[derive(Clone, Debug, Serialize, Deserialize)]
60pub struct RoomInfoV1 {
61    room_id: OwnedRoomId,
62    room_type: RoomState,
63    notification_counts: UnreadNotificationsCount,
64    summary: RoomSummary,
65    members_synced: bool,
66    last_prev_batch: Option<String>,
67    #[serde(default = "sync_info_complete")] // see fn docs for why we use this default
68    sync_info: SyncInfo,
69    #[serde(default = "encryption_state_default")] // see fn docs for why we use this default
70    encryption_state_synced: bool,
71    latest_event: Option<TimelineEvent>,
72    base_info: BaseRoomInfoV1,
73}
74
75impl RoomInfoV1 {
76    /// Get the room ID of this room.
77    pub fn room_id(&self) -> &RoomId {
78        &self.room_id
79    }
80
81    /// Returns the state this room is in.
82    pub fn state(&self) -> RoomState {
83        self.room_type
84    }
85
86    /// Migrate this to a [`RoomInfo`], using the given `m.room.create` event
87    /// from the room state.
88    pub fn migrate(self, create: Option<&SyncOrStrippedState<RoomCreateEventContent>>) -> RoomInfo {
89        let RoomInfoV1 {
90            room_id,
91            room_type,
92            notification_counts,
93            summary,
94            members_synced,
95            last_prev_batch,
96            sync_info,
97            encryption_state_synced,
98            latest_event: _, // deprecated
99            base_info,
100        } = self;
101
102        RoomInfo {
103            data_format_version: 0,
104            room_id,
105            room_state: room_type,
106            notification_counts,
107            summary,
108            members_synced,
109            last_prev_batch,
110            sync_info,
111            encryption_state_synced,
112            latest_event_value: LatestEventValue::None,
113            read_receipts: Default::default(),
114            base_info: base_info.migrate(create),
115            warned_about_unknown_room_version_rules: Arc::new(false.into()),
116            cached_display_name: None,
117            cached_user_defined_notification_mode: None,
118            recency_stamp: None,
119            invite_acceptance_details: None,
120        }
121    }
122}
123
124// The sync_info field introduced a new field in the database schema, for
125// backwards compatibility we assume that if the room is in the database, yet
126// the field isn't, we have synced it before this field was introduced - which
127// was a a full sync.
128fn sync_info_complete() -> SyncInfo {
129    SyncInfo::FullySynced
130}
131
132// The encryption_state_synced field introduced a new field in the database
133// schema, for backwards compatibility we assume that if the room is in the
134// database, yet the field isn't, we have synced it before this field was
135// introduced - which was a a full sync.
136fn encryption_state_default() -> bool {
137    true
138}
139
140/// [`BaseRoomInfo`] version 1.
141#[derive(Clone, Debug, Serialize, Deserialize)]
142struct BaseRoomInfoV1 {
143    avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
144    canonical_alias: Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
145    dm_targets: HashSet<OwnedUserId>,
146    encryption: Option<RoomEncryptionEventContent>,
147    guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
148    history_visibility:
149        Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
150    join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
151    max_power_level: i64,
152    name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
153    tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
154    topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
155}
156
157impl BaseRoomInfoV1 {
158    /// Migrate this to a [`BaseRoomInfo`].
159    fn migrate(
160        self,
161        create: Option<&SyncOrStrippedState<RoomCreateEventContent>>,
162    ) -> Box<BaseRoomInfo> {
163        let BaseRoomInfoV1 {
164            avatar,
165            canonical_alias,
166            dm_targets,
167            encryption,
168            guest_access,
169            history_visibility,
170            join_rules,
171            max_power_level,
172            name,
173            tombstone,
174            topic,
175        } = self;
176
177        let create = create.map(|ev| match ev {
178            SyncOrStrippedState::Sync(e) => e.into(),
179            SyncOrStrippedState::Stripped(e) => e.into(),
180        });
181
182        let mut converted_dm_targets = HashSet::new();
183        for dm_target in dm_targets {
184            converted_dm_targets.insert(OwnedDirectUserIdentifier::from(dm_target));
185        }
186
187        Box::new(BaseRoomInfo {
188            avatar,
189            canonical_alias,
190            create,
191            dm_targets: converted_dm_targets,
192            encryption,
193            guest_access,
194            history_visibility,
195            join_rules,
196            max_power_level,
197            name,
198            tombstone,
199            topic,
200            ..Default::default()
201        })
202    }
203}