1use std::{
16 collections::{HashMap, HashSet},
17 sync::Arc,
18};
19
20use gloo_utils::format::JsValueSerdeExt;
21use indexed_db_futures::{
22 database::{Database, VersionChangeEvent},
23 error::Error,
24 future::OpenDbRequest,
25 object_store::ObjectStore,
26 prelude::*,
27 transaction::{Transaction, TransactionMode},
28};
29use js_sys::Date as JsDate;
30use matrix_sdk_base::{
31 StateStoreDataKey, deserialized_responses::SyncOrStrippedState,
32 store::migration_helpers::RoomInfoV1,
33};
34use matrix_sdk_store_encryption::StoreCipher;
35use ruma::{
36 events::{
37 StateEventType,
38 room::{
39 create::RoomCreateEventContent,
40 member::{StrippedRoomMemberEvent, SyncRoomMemberEvent},
41 },
42 },
43 serde::Raw,
44};
45use serde::{Deserialize, Serialize};
46use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
47use wasm_bindgen::JsValue;
48
49use super::{
50 ALL_STORES, Result, RoomMember, deserialize_value, encode_key, encode_to_range, keys,
51 serialize_value,
52};
53use crate::IndexeddbStateStoreError;
54
55const CURRENT_DB_VERSION: u32 = 16;
56const CURRENT_META_DB_VERSION: u32 = 2;
57
58#[derive(Clone, Debug, PartialEq, Eq)]
61pub enum MigrationConflictStrategy {
62 Drop,
64 Raise,
68 BackupAndDrop,
70}
71
72#[derive(Clone, Serialize, Deserialize)]
73struct StoreKeyWrapper(Vec<u8>);
74
75mod old_keys {
76 pub const SESSION: &str = "session";
77 pub const SYNC_TOKEN: &str = "sync_token";
78 pub const MEMBERS: &str = "members";
79 pub const STRIPPED_MEMBERS: &str = "stripped_members";
80 pub const JOINED_USER_IDS: &str = "joined_user_ids";
81 pub const INVITED_USER_IDS: &str = "invited_user_ids";
82 pub const STRIPPED_JOINED_USER_IDS: &str = "stripped_joined_user_ids";
83 pub const STRIPPED_INVITED_USER_IDS: &str = "stripped_invited_user_ids";
84 pub const STRIPPED_ROOM_INFOS: &str = "stripped_room_infos";
85 pub const MEDIA: &str = "media";
86}
87
88pub async fn upgrade_meta_db(
89 meta_name: &str,
90 passphrase: Option<&str>,
91) -> Result<(Database, Option<Arc<StoreCipher>>)> {
92 let db_req: OpenDbRequest = Database::open(meta_name)
94 .with_version(CURRENT_META_DB_VERSION)
95 .with_on_upgrade_needed(
96 |evt: VersionChangeEvent, tx: &Transaction<'_>| -> Result<(), Error> {
97 let db = tx.db();
98 let old_version = evt.old_version() as u32;
99
100 if old_version < 1 {
101 db.create_object_store(keys::INTERNAL_STATE).build()?;
102 }
103
104 if old_version < 2 {
105 db.create_object_store(keys::BACKUPS_META).build()?;
106 }
107
108 Ok(())
109 },
110 )
111 .build()?;
112
113 let meta_db: Database = db_req.await?;
114
115 let store_cipher = if let Some(passphrase) = passphrase {
116 let tx: Transaction<'_> = meta_db
117 .transaction(keys::INTERNAL_STATE)
118 .with_mode(TransactionMode::Readwrite)
119 .build()?;
120 let ob = tx.object_store(keys::INTERNAL_STATE)?;
121
122 let cipher = if let Some(StoreKeyWrapper(inner)) = ob
123 .get(&JsValue::from_str(keys::STORE_KEY))
124 .await?
125 .map(|v: JsValue| v.into_serde())
126 .transpose()?
127 {
128 StoreCipher::import(passphrase, &inner)?
129 } else {
130 let cipher = StoreCipher::new()?;
131 #[cfg(not(test))]
132 let export = cipher.export(passphrase)?;
133 #[cfg(test)]
134 let export = cipher._insecure_export_fast_for_testing(passphrase)?;
135 ob.put(&StoreKeyWrapper(export)).with_key(keys::STORE_KEY.to_owned()).serde()?.await?;
136 cipher
137 };
138
139 tx.commit().await?;
140 Some(Arc::new(cipher))
141 } else {
142 None
143 };
144
145 Ok((meta_db, store_cipher))
146}
147
148#[derive(Debug, Clone, Default)]
150pub struct OngoingMigration {
151 drop_stores: HashSet<&'static str>,
153 create_stores: HashSet<&'static str>,
155 data: HashMap<&'static str, Vec<(JsValue, JsValue)>>,
157}
158
159pub async fn upgrade_inner_db(
160 name: &str,
161 store_cipher: Option<&StoreCipher>,
162 migration_strategy: MigrationConflictStrategy,
163 meta_db: &Database,
164) -> Result<Database> {
165 let mut db = Database::open(name).await?;
166
167 let mut old_version = db.version() as u32;
171
172 if old_version < CURRENT_DB_VERSION {
173 let has_store_cipher = store_cipher.is_some();
180
181 if old_version == 1 && db.object_store_names().next().is_none() {
185 old_version = 0;
186 }
187
188 if old_version == 0 {
192 let migration = OngoingMigration {
193 create_stores: ALL_STORES.iter().copied().collect(),
194 ..Default::default()
195 };
196 db = apply_migration(db, CURRENT_DB_VERSION, migration).await?;
197 } else if old_version < 2 && has_store_cipher {
198 match migration_strategy {
199 MigrationConflictStrategy::BackupAndDrop => {
200 backup_v1(&db, meta_db).await?;
201 }
202 MigrationConflictStrategy::Drop => {}
203 MigrationConflictStrategy::Raise => {
204 return Err(IndexeddbStateStoreError::MigrationConflict {
205 name: name.to_owned(),
206 old_version,
207 new_version: CURRENT_DB_VERSION,
208 });
209 }
210 }
211
212 let migration = OngoingMigration {
213 drop_stores: V1_STORES.iter().copied().collect(),
214 create_stores: ALL_STORES.iter().copied().collect(),
215 ..Default::default()
216 };
217 db = apply_migration(db, CURRENT_DB_VERSION, migration).await?;
218 } else {
219 if old_version < 3 {
220 db = migrate_to_v3(db, store_cipher).await?;
221 }
222 if old_version < 4 {
223 db = migrate_to_v4(db, store_cipher).await?;
224 }
225 if old_version < 5 {
226 db = migrate_to_v5(db, store_cipher).await?;
227 }
228 if old_version < 6 {
229 db = migrate_to_v6(db, store_cipher).await?;
230 }
231 if old_version < 7 {
232 db = migrate_to_v7(db, store_cipher).await?;
233 }
234 if old_version < 8 {
235 db = migrate_to_v8(db, store_cipher).await?;
236 }
237 if old_version < 9 {
238 db = migrate_to_v9(db).await?;
239 }
240 if old_version < 10 {
241 db = migrate_to_v10(db).await?;
242 }
243 if old_version < 11 {
244 db = migrate_to_v11(db).await?;
245 }
246 if old_version < 12 {
247 db = migrate_to_v12(db).await?;
248 }
249 if old_version < 13 {
250 db = migrate_to_v13(db).await?;
251 }
252 if old_version < 14 {
253 db = migrate_to_v14(db).await?;
254 }
255 if old_version < 15 {
256 db = migrate_to_v15(db).await?;
257 }
258 if old_version < 16 {
259 db = migrate_to_v16(db).await?;
260 }
261 }
262
263 db.close();
264
265 let db_req: OpenDbRequest = Database::open(name)
266 .with_version(CURRENT_DB_VERSION)
267 .with_on_upgrade_needed(
268 move |evt: VersionChangeEvent, _: &Transaction<'_>| -> Result<(), Error> {
269 panic!(
273 "Opening database that was not fully upgraded: \
274 DB version: {}; latest version: {CURRENT_DB_VERSION}",
275 evt.old_version()
276 )
277 },
278 )
279 .build()?;
280 db = db_req.await?;
281 }
282
283 Ok(db)
284}
285
286async fn apply_migration(
289 db: Database,
290 version: u32,
291 migration: OngoingMigration,
292) -> Result<Database> {
293 let name = db.name();
294 db.close();
295
296 let db_req: OpenDbRequest = Database::open(&name)
297 .with_version(version)
298 .with_on_upgrade_needed(
299 move |_: VersionChangeEvent, tx: &Transaction<'_>| -> Result<(), Error> {
300 for store in &migration.drop_stores {
302 tx.db().delete_object_store(store)?;
303 }
304 for store in &migration.create_stores {
305 tx.db().create_object_store(store).build()?;
306 }
307
308 Ok(())
309 },
310 )
311 .build()?;
312
313 let db = db_req.await?;
314
315 if !migration.data.is_empty() {
317 let stores: Vec<_> = migration.data.keys().copied().collect();
318 let tx = db.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
319
320 for (name, data) in migration.data {
321 let store = tx.object_store(name)?;
322 for (key, value) in data {
323 store.put(&value).with_key(key).await?;
324 }
325 }
326
327 tx.commit().await?;
328 }
329
330 Ok(db)
331}
332
333pub const V1_STORES: &[&str] = &[
334 old_keys::SESSION,
335 keys::ACCOUNT_DATA,
336 old_keys::MEMBERS,
337 keys::PROFILES,
338 keys::DISPLAY_NAMES,
339 old_keys::JOINED_USER_IDS,
340 old_keys::INVITED_USER_IDS,
341 keys::ROOM_STATE,
342 keys::ROOM_INFOS,
343 keys::PRESENCE,
344 keys::ROOM_ACCOUNT_DATA,
345 old_keys::STRIPPED_ROOM_INFOS,
346 old_keys::STRIPPED_MEMBERS,
347 keys::STRIPPED_ROOM_STATE,
348 old_keys::STRIPPED_JOINED_USER_IDS,
349 old_keys::STRIPPED_INVITED_USER_IDS,
350 keys::ROOM_USER_RECEIPTS,
351 keys::ROOM_EVENT_RECEIPTS,
352 old_keys::MEDIA,
353 keys::CUSTOM,
354 old_keys::SYNC_TOKEN,
355];
356
357async fn backup_v1(source: &Database, meta: &Database) -> Result<()> {
358 let now = JsDate::now();
359 let backup_name = format!("backup-{}-{now}", source.name());
360
361 let db_req: OpenDbRequest = Database::open(&backup_name)
362 .with_version(source.version())
363 .with_on_upgrade_needed(
364 move |_: VersionChangeEvent, tx: &Transaction<'_>| -> Result<(), Error> {
365 let db = tx.db();
367 for name in V1_STORES {
368 db.create_object_store(name).build()?;
369 }
370 Ok(())
371 },
372 )
373 .build()?;
374 let target = db_req.await?;
375
376 for name in V1_STORES {
377 let source_tx = source.transaction(*name).with_mode(TransactionMode::Readonly).build()?;
378 let source_obj = source_tx.object_store(name)?;
379 let Some(mut curs) = source_obj.open_cursor().await? else {
380 continue;
381 };
382
383 let mut data = vec![];
384 while let Some(value) = curs.next_record::<JsValue>().await? {
385 if let Some(key) = curs.key::<JsValue>()? {
386 data.push((key, value));
387 }
388 }
389
390 let target_tx = target.transaction(*name).with_mode(TransactionMode::Readwrite).build()?;
391 let target_obj = target_tx.object_store(name)?;
392
393 for (key, value) in data {
394 target_obj.put(value).with_key(key).await?;
395 }
396
397 target_tx.commit().await?;
398 }
399
400 let tx = meta.transaction(keys::BACKUPS_META).with_mode(TransactionMode::Readwrite).build()?;
401 let backup_store = tx.object_store(keys::BACKUPS_META)?;
402 backup_store.put(&backup_name).with_key(now).await?;
403
404 tx.commit().await?;
405
406 target.close();
407
408 Ok(())
409}
410
411async fn v3_fix_store(store: &ObjectStore<'_>, store_cipher: Option<&StoreCipher>) -> Result<()> {
412 fn maybe_fix_json(raw_json: &RawJsonValue) -> Result<Option<JsonValue>> {
413 let json = raw_json.get();
414
415 if json.contains(r#""content":null"#) {
416 let mut value: JsonValue = serde_json::from_str(json)?;
417 if let Some(content) = value.get_mut("content")
418 && matches!(content, JsonValue::Null)
419 {
420 *content = JsonValue::Object(Default::default());
421 return Ok(Some(value));
422 }
423 }
424
425 Ok(None)
426 }
427
428 let cursor = store.open_cursor().await?;
429
430 if let Some(mut cursor) = cursor {
431 while let Some(value) = cursor.next_record().await? {
432 let raw_json: Box<RawJsonValue> = deserialize_value(store_cipher, &value)?;
433
434 if let Some(fixed_json) = maybe_fix_json(&raw_json)? {
435 cursor.update(&serialize_value(store_cipher, &fixed_json)?).await?;
436 }
437 }
438 }
439
440 Ok(())
441}
442
443async fn migrate_to_v3(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
445 let tx = db
446 .transaction([keys::ROOM_STATE, keys::ROOM_INFOS])
447 .with_mode(TransactionMode::Readwrite)
448 .build()?;
449
450 v3_fix_store(&tx.object_store(keys::ROOM_STATE)?, store_cipher).await?;
451 v3_fix_store(&tx.object_store(keys::ROOM_INFOS)?, store_cipher).await?;
452
453 tx.commit().await?;
454
455 let name = db.name();
456 db.close();
457
458 Ok(Database::open(&name).with_version(3u32).await?)
460}
461
462async fn migrate_to_v4(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
464 let tx = db
465 .transaction([old_keys::SYNC_TOKEN, old_keys::SESSION])
466 .with_mode(TransactionMode::Readonly)
467 .build()?;
468 let mut values = Vec::new();
469
470 let sync_token_store = tx.object_store(old_keys::SYNC_TOKEN)?;
472 let sync_token = sync_token_store.get(&JsValue::from_str(old_keys::SYNC_TOKEN)).await?;
473
474 if let Some(sync_token) = sync_token {
475 values.push((
476 encode_key(store_cipher, StateStoreDataKey::SYNC_TOKEN, StateStoreDataKey::SYNC_TOKEN),
477 sync_token,
478 ));
479 }
480
481 let session_store = tx.object_store(old_keys::SESSION)?;
483 let range = encode_to_range(store_cipher, StateStoreDataKey::FILTER, StateStoreDataKey::FILTER);
484 if let Some(mut cursor) = session_store.open_cursor().with_query(&range).await? {
485 while let Some(value) = cursor.next_record().await? {
486 let Some(key) = cursor.key()? else {
487 break;
488 };
489 values.push((key, value));
490 }
491 }
492
493 tx.commit().await?;
494
495 let mut data = HashMap::new();
496 if !values.is_empty() {
497 data.insert(keys::KV, values);
498 }
499
500 let migration = OngoingMigration {
501 drop_stores: [old_keys::SYNC_TOKEN, old_keys::SESSION].into_iter().collect(),
502 create_stores: [keys::KV].into_iter().collect(),
503 data,
504 };
505 apply_migration(db, 4, migration).await
506}
507
508async fn migrate_to_v5(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
510 let tx = db
511 .transaction([
512 old_keys::MEMBERS,
513 old_keys::STRIPPED_MEMBERS,
514 keys::ROOM_STATE,
515 keys::STRIPPED_ROOM_STATE,
516 keys::ROOM_INFOS,
517 old_keys::STRIPPED_ROOM_INFOS,
518 ])
519 .with_mode(TransactionMode::Readwrite)
520 .build()?;
521
522 let members_store = tx.object_store(old_keys::MEMBERS)?;
523 let state_store = tx.object_store(keys::ROOM_STATE)?;
524 let room_infos = tx
525 .object_store(keys::ROOM_INFOS)?
526 .get_all()
527 .await?
528 .filter_map(Result::ok)
529 .filter_map(|f| deserialize_value::<RoomInfoV1>(store_cipher, &f).ok())
530 .collect::<Vec<_>>();
531
532 for room_info in room_infos {
533 let room_id = room_info.room_id();
534 let range = encode_to_range(store_cipher, old_keys::MEMBERS, room_id);
535 for result in members_store.get_all().with_query(&range).await? {
536 let value = result?;
537 let raw_member_event =
538 deserialize_value::<Raw<SyncRoomMemberEvent>>(store_cipher, &value)?;
539 let state_key = raw_member_event.get_field::<String>("state_key")?.unwrap_or_default();
540 let key = encode_key(
541 store_cipher,
542 keys::ROOM_STATE,
543 (room_id, StateEventType::RoomMember, state_key),
544 );
545
546 state_store.add(&value).with_key(key).build()?.await?;
547 }
548 }
549
550 let stripped_members_store = tx.object_store(old_keys::STRIPPED_MEMBERS)?;
551 let stripped_state_store = tx.object_store(keys::STRIPPED_ROOM_STATE)?;
552 let stripped_room_infos = tx
553 .object_store(old_keys::STRIPPED_ROOM_INFOS)?
554 .get_all()
555 .await?
556 .filter_map(Result::ok)
557 .filter_map(|f| deserialize_value::<RoomInfoV1>(store_cipher, &f).ok())
558 .collect::<Vec<_>>();
559
560 for room_info in stripped_room_infos {
561 let room_id = room_info.room_id();
562 let range = encode_to_range(store_cipher, old_keys::STRIPPED_MEMBERS, room_id);
563 for result in stripped_members_store.get_all().with_query(&range).await? {
564 let value = result?;
565 let raw_member_event =
566 deserialize_value::<Raw<StrippedRoomMemberEvent>>(store_cipher, &value)?;
567 let state_key = raw_member_event.get_field::<String>("state_key")?.unwrap_or_default();
568 let key = encode_key(
569 store_cipher,
570 keys::STRIPPED_ROOM_STATE,
571 (room_id, StateEventType::RoomMember, state_key),
572 );
573
574 stripped_state_store.add(&value).with_key(key).build()?.await?;
575 }
576 }
577
578 tx.commit().await?;
579
580 let migration = OngoingMigration {
581 drop_stores: [old_keys::MEMBERS, old_keys::STRIPPED_MEMBERS].into_iter().collect(),
582 create_stores: Default::default(),
583 data: Default::default(),
584 };
585 apply_migration(db, 5, migration).await
586}
587
588async fn migrate_to_v6(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
590 let tx = db
593 .transaction([
594 keys::ROOM_STATE,
595 keys::ROOM_INFOS,
596 keys::STRIPPED_ROOM_STATE,
597 old_keys::STRIPPED_ROOM_INFOS,
598 ])
599 .with_mode(TransactionMode::Readonly)
600 .build()?;
601
602 let state_store = tx.object_store(keys::ROOM_STATE)?;
603 let room_infos = tx
604 .object_store(keys::ROOM_INFOS)?
605 .get_all()
606 .await?
607 .filter_map(Result::ok)
608 .filter_map(|f| deserialize_value::<RoomInfoV1>(store_cipher, &f).ok())
609 .collect::<Vec<_>>();
610 let mut values = Vec::new();
611
612 for room_info in room_infos {
613 let room_id = room_info.room_id();
614 let range =
615 encode_to_range(store_cipher, keys::ROOM_STATE, (room_id, StateEventType::RoomMember));
616 for result in state_store.get_all().with_query(&range).await? {
617 let value = result?;
618 let member_event = deserialize_value::<Raw<SyncRoomMemberEvent>>(store_cipher, &value)?
619 .deserialize()?;
620 let key = encode_key(store_cipher, keys::USER_IDS, (room_id, member_event.state_key()));
621 let value = serialize_value(store_cipher, &RoomMember::from(&member_event))?;
622
623 values.push((key, value));
624 }
625 }
626
627 let stripped_state_store = tx.object_store(keys::STRIPPED_ROOM_STATE)?;
628 let stripped_room_infos = tx
629 .object_store(old_keys::STRIPPED_ROOM_INFOS)?
630 .get_all()
631 .await?
632 .filter_map(Result::ok)
633 .filter_map(|f| deserialize_value::<RoomInfoV1>(store_cipher, &f).ok())
634 .collect::<Vec<_>>();
635 let mut stripped_values = Vec::new();
636
637 for room_info in stripped_room_infos {
638 let room_id = room_info.room_id();
639 let range = encode_to_range(
640 store_cipher,
641 keys::STRIPPED_ROOM_STATE,
642 (room_id, StateEventType::RoomMember),
643 );
644 for result in stripped_state_store.get_all().with_query(&range).await? {
645 let value = result?;
646 let stripped_member_event =
647 deserialize_value::<Raw<StrippedRoomMemberEvent>>(store_cipher, &value)?
648 .deserialize()?;
649 let key = encode_key(
650 store_cipher,
651 keys::STRIPPED_USER_IDS,
652 (room_id, &stripped_member_event.state_key),
653 );
654 let value = serialize_value(store_cipher, &RoomMember::from(&stripped_member_event))?;
655
656 stripped_values.push((key, value));
657 }
658 }
659
660 tx.commit().await?;
661
662 let mut data = HashMap::new();
663 if !values.is_empty() {
664 data.insert(keys::USER_IDS, values);
665 }
666 if !stripped_values.is_empty() {
667 data.insert(keys::STRIPPED_USER_IDS, stripped_values);
668 }
669
670 let migration = OngoingMigration {
671 drop_stores: HashSet::from_iter([
672 old_keys::JOINED_USER_IDS,
673 old_keys::INVITED_USER_IDS,
674 old_keys::STRIPPED_JOINED_USER_IDS,
675 old_keys::STRIPPED_INVITED_USER_IDS,
676 ]),
677 create_stores: HashSet::from_iter([keys::USER_IDS, keys::STRIPPED_USER_IDS]),
678 data,
679 };
680 apply_migration(db, 6, migration).await
681}
682
683async fn migrate_to_v7(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
686 let tx = db
687 .transaction([old_keys::STRIPPED_ROOM_INFOS])
688 .with_mode(TransactionMode::Readonly)
689 .build()?;
690
691 let room_infos = tx
692 .object_store(old_keys::STRIPPED_ROOM_INFOS)?
693 .get_all()
694 .await?
695 .filter_map(Result::ok)
696 .filter_map(|value| {
697 deserialize_value::<RoomInfoV1>(store_cipher, &value)
698 .ok()
699 .map(|info| (encode_key(store_cipher, keys::ROOM_INFOS, info.room_id()), value))
700 })
701 .collect::<Vec<_>>();
702
703 tx.commit().await?;
704
705 let mut data = HashMap::new();
706 if !room_infos.is_empty() {
707 data.insert(keys::ROOM_INFOS, room_infos);
708 }
709
710 let migration = OngoingMigration {
711 drop_stores: HashSet::from_iter([old_keys::STRIPPED_ROOM_INFOS]),
712 data,
713 ..Default::default()
714 };
715 apply_migration(db, 7, migration).await
716}
717
718async fn migrate_to_v8(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
720 let tx = db
721 .transaction([keys::ROOM_STATE, keys::STRIPPED_ROOM_STATE, keys::ROOM_INFOS])
722 .with_mode(TransactionMode::Readwrite)
723 .build()?;
724
725 let room_state_store = tx.object_store(keys::ROOM_STATE)?;
726 let stripped_room_state_store = tx.object_store(keys::STRIPPED_ROOM_STATE)?;
727 let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
728
729 let room_infos_v1 = room_infos_store
730 .get_all()
731 .build()?
732 .await?
733 .map(|value| deserialize_value::<RoomInfoV1>(store_cipher, &value?))
734 .collect::<Result<Vec<_>, _>>()?;
735
736 for room_info_v1 in room_infos_v1 {
737 let create = if let Some(event) = stripped_room_state_store
738 .get(&encode_key(
739 store_cipher,
740 keys::STRIPPED_ROOM_STATE,
741 (room_info_v1.room_id(), &StateEventType::RoomCreate, ""),
742 ))
743 .await?
744 .map(|f| deserialize_value(store_cipher, &f))
745 .transpose()?
746 {
747 Some(SyncOrStrippedState::<RoomCreateEventContent>::Stripped(event))
748 } else {
749 room_state_store
750 .get(&encode_key(
751 store_cipher,
752 keys::ROOM_STATE,
753 (room_info_v1.room_id(), &StateEventType::RoomCreate, ""),
754 ))
755 .await?
756 .map(|f| deserialize_value(store_cipher, &f))
757 .transpose()?
758 .map(SyncOrStrippedState::<RoomCreateEventContent>::Sync)
759 };
760
761 let room_info = room_info_v1.migrate(create.as_ref());
762 room_infos_store
763 .put(&serialize_value(store_cipher, &room_info)?)
764 .with_key(encode_key(store_cipher, keys::ROOM_INFOS, room_info.room_id()))
765 .build()?;
766 }
767
768 tx.commit().await?;
769
770 let name = db.name();
771 db.close();
772
773 Ok(Database::open(&name).with_version(8u32).build()?.await?)
775}
776
777async fn migrate_to_v9(db: Database) -> Result<Database> {
779 let migration = OngoingMigration {
780 drop_stores: [].into(),
781 create_stores: [keys::ROOM_SEND_QUEUE].into_iter().collect(),
782 data: Default::default(),
783 };
784 apply_migration(db, 9, migration).await
785}
786
787async fn migrate_to_v10(db: Database) -> Result<Database> {
789 let migration = OngoingMigration {
790 drop_stores: [].into(),
791 create_stores: [keys::DEPENDENT_SEND_QUEUE].into_iter().collect(),
792 data: Default::default(),
793 };
794 apply_migration(db, 10, migration).await
795}
796
797async fn migrate_to_v11(db: Database) -> Result<Database> {
799 let migration = OngoingMigration {
800 drop_stores: [old_keys::MEDIA].into(),
801 create_stores: Default::default(),
802 data: Default::default(),
803 };
804 apply_migration(db, 11, migration).await
805}
806
807async fn migrate_to_v12(db: Database) -> Result<Database> {
810 let store_keys = [keys::DEPENDENT_SEND_QUEUE, keys::ROOM_SEND_QUEUE];
811 let tx = db.transaction(store_keys).with_mode(TransactionMode::Readwrite).build()?;
812
813 for store_name in store_keys {
814 let store = tx.object_store(store_name)?;
815 store.clear()?;
816 }
817
818 tx.commit().await?;
819
820 let name = db.name();
821 db.close();
822
823 Ok(Database::open(&name).with_version(12u32).build()?.await?)
825}
826
827async fn migrate_to_v13(db: Database) -> Result<Database> {
829 let migration = OngoingMigration {
830 drop_stores: [].into(),
831 create_stores: [keys::THREAD_SUBSCRIPTIONS].into_iter().collect(),
832 data: Default::default(),
833 };
834 apply_migration(db, 13, migration).await
835}
836
837async fn migrate_to_v14(db: Database) -> Result<Database> {
841 let migration = OngoingMigration {
842 drop_stores: [keys::THREAD_SUBSCRIPTIONS].into_iter().collect(),
843 create_stores: [keys::THREAD_SUBSCRIPTIONS].into_iter().collect(),
844 data: Default::default(),
845 };
846 apply_migration(db, 14, migration).await
847}
848
849async fn migrate_to_v15(db: Database) -> Result<Database> {
851 let migration = OngoingMigration {
852 drop_stores: Default::default(),
853 create_stores: [keys::GLOBAL_PROFILES].into_iter().collect(),
854 data: Default::default(),
855 };
856 apply_migration(db, 15, migration).await
857}
858
859async fn migrate_to_v16(db: Database) -> Result<Database> {
861 let migration = OngoingMigration {
862 drop_stores: Default::default(),
863 create_stores: [keys::UNAVAILABLE_GLOBAL_PROFILES].into_iter().collect(),
864 data: Default::default(),
865 };
866 apply_migration(db, 16, migration).await
867}
868
869#[cfg(all(test, target_family = "wasm"))]
870mod tests {
871 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
872
873 use assert_matches::assert_matches;
874 use indexed_db_futures::{
875 database::{Database, VersionChangeEvent},
876 error::Error,
877 object_store::ObjectStore,
878 prelude::*,
879 transaction::{Transaction, TransactionMode},
880 };
881 use matrix_sdk_base::{
882 RoomMemberships, RoomState, StateStore, StateStoreDataKey, StoreError,
883 deserialized_responses::RawMemberEvent,
884 store::{RoomLoadSettings, StateStoreExt},
885 sync::UnreadNotificationsCount,
886 };
887 use matrix_sdk_test::{async_test, event_factory::EventFactory};
888 use ruma::{
889 EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, UserId,
890 events::{
891 AnySyncStateEvent, StateEventType,
892 room::{
893 create::RoomCreateEventContent,
894 member::{StrippedRoomMemberEvent, SyncRoomMemberEvent},
895 },
896 },
897 owned_user_id, room_id,
898 serde::Raw,
899 server_name, user_id,
900 };
901 use serde_json::json;
902 use strass::assert_let;
903 use uuid::Uuid;
904 use wasm_bindgen::JsValue;
905
906 use super::{CURRENT_DB_VERSION, CURRENT_META_DB_VERSION, MigrationConflictStrategy, old_keys};
907 use crate::{
908 IndexeddbStateStore, IndexeddbStateStoreError,
909 serializer::safe_encode::traits::SafeEncode,
910 state_store::{Result, encode_key, keys, serialize_value},
911 };
912
913 const CUSTOM_DATA_KEY: &[u8] = b"custom_data_key";
914 const CUSTOM_DATA: &[u8] = b"some_custom_data";
915
916 pub async fn create_fake_db(name: &str, version: u32) -> Result<Database> {
917 Database::open(name)
918 .with_version(version)
919 .with_on_upgrade_needed(
920 move |_: VersionChangeEvent, tx: &Transaction<'_>| -> Result<(), Error> {
921 let db = tx.db();
922
923 let common_stores = &[
925 keys::ACCOUNT_DATA,
926 keys::PROFILES,
927 keys::DISPLAY_NAMES,
928 keys::ROOM_STATE,
929 keys::ROOM_INFOS,
930 keys::PRESENCE,
931 keys::ROOM_ACCOUNT_DATA,
932 keys::STRIPPED_ROOM_STATE,
933 keys::ROOM_USER_RECEIPTS,
934 keys::ROOM_EVENT_RECEIPTS,
935 keys::CUSTOM,
936 ];
937
938 for name in common_stores {
939 db.create_object_store(name).build()?;
940 }
941
942 if version < 4 {
943 for name in [old_keys::SYNC_TOKEN, old_keys::SESSION] {
944 db.create_object_store(name).build()?;
945 }
946 }
947 if version >= 4 {
948 db.create_object_store(keys::KV).build()?;
949 }
950 if version < 5 {
951 for name in [old_keys::MEMBERS, old_keys::STRIPPED_MEMBERS] {
952 db.create_object_store(name).build()?;
953 }
954 }
955 if version < 6 {
956 for name in [
957 old_keys::INVITED_USER_IDS,
958 old_keys::JOINED_USER_IDS,
959 old_keys::STRIPPED_INVITED_USER_IDS,
960 old_keys::STRIPPED_JOINED_USER_IDS,
961 ] {
962 db.create_object_store(name).build()?;
963 }
964 }
965 if version >= 6 {
966 for name in [keys::USER_IDS, keys::STRIPPED_USER_IDS] {
967 db.create_object_store(name).build()?;
968 }
969 }
970 if version < 7 {
971 db.create_object_store(old_keys::STRIPPED_ROOM_INFOS).build()?;
972 }
973 if version < 11 {
974 db.create_object_store(old_keys::MEDIA).build()?;
975 }
976
977 Ok(())
978 },
979 )
980 .build()?
981 .await
982 .map_err(Into::into)
983 }
984
985 fn room_info_v1_json(
986 room_id: &RoomId,
987 state: RoomState,
988 name: Option<&str>,
989 creator: Option<&UserId>,
990 ) -> serde_json::Value {
991 let name_content = match name {
993 Some(name) => json!({ "name": name }),
994 None => json!({ "name": null }),
995 };
996 let create_content = match creator {
998 Some(creator) => RoomCreateEventContent::new_v1(creator.to_owned()),
999 None => RoomCreateEventContent::new_v11(),
1000 };
1001
1002 json!({
1003 "room_id": room_id,
1004 "room_type": state,
1005 "notification_counts": UnreadNotificationsCount::default(),
1006 "summary": {
1007 "heroes": [],
1008 "joined_member_count": 0,
1009 "invited_member_count": 0,
1010 },
1011 "members_synced": false,
1012 "base_info": {
1013 "dm_targets": [],
1014 "max_power_level": 100,
1015 "name": {
1016 "Original": {
1017 "content": name_content,
1018 },
1019 },
1020 "create": {
1021 "Original": {
1022 "content": create_content,
1023 }
1024 }
1025 },
1026 })
1027 }
1028
1029 #[async_test]
1030 pub async fn test_new_store() -> Result<()> {
1031 let name = format!("new-store-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
1032
1033 let store = IndexeddbStateStore::builder().name(name).build().await?;
1035 assert_eq!(store.has_backups().await?, false);
1037 assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1039
1040 assert_eq!(store.version(), CURRENT_DB_VERSION);
1042 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1043
1044 Ok(())
1045 }
1046
1047 #[async_test]
1048 pub async fn test_migrating_v1_to_v2_plain() -> Result<()> {
1049 let name = format!("migrating-v2-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
1050
1051 {
1053 let db = create_fake_db(&name, 1).await?;
1054 let tx = db.transaction(keys::CUSTOM).with_mode(TransactionMode::Readwrite).build()?;
1055 let custom = tx.object_store(keys::CUSTOM)?;
1056 let jskey = JsValue::from_str(
1057 core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1058 );
1059 custom.put(&serialize_value(None, &CUSTOM_DATA)?).with_key(jskey).await?;
1060 tx.commit().await?;
1061 db.close();
1062 }
1063
1064 let store = IndexeddbStateStore::builder().name(name).build().await?;
1066 assert_eq!(store.has_backups().await?, false);
1068 assert_let!(Some(stored_data) = store.get_custom_value(CUSTOM_DATA_KEY).await?);
1070 assert_eq!(stored_data, CUSTOM_DATA);
1071
1072 assert_eq!(store.version(), CURRENT_DB_VERSION);
1074 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1075
1076 Ok(())
1077 }
1078
1079 #[async_test]
1080 pub async fn test_migrating_v1_to_v2_with_pw() -> Result<()> {
1081 let name =
1082 format!("migrating-v2-with-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
1083 let passphrase = "somepassphrase".to_owned();
1084
1085 {
1087 let db = create_fake_db(&name, 1).await?;
1088 let tx = db.transaction(keys::CUSTOM).with_mode(TransactionMode::Readwrite).build()?;
1089 let custom = tx.object_store(keys::CUSTOM)?;
1090 let jskey = JsValue::from_str(
1091 core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1092 );
1093 custom.put(&serialize_value(None, &CUSTOM_DATA)?).with_key(jskey).await?;
1094 tx.commit().await?;
1095 db.close();
1096 }
1097
1098 let store =
1100 IndexeddbStateStore::builder().name(name).passphrase(passphrase).build().await?;
1101 assert_eq!(store.has_backups().await?, true);
1103 assert!(store.latest_backup().await?.is_some(), "No backup_found");
1104 assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1106
1107 assert_eq!(store.version(), CURRENT_DB_VERSION);
1109 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1110
1111 Ok(())
1112 }
1113
1114 #[async_test]
1115 pub async fn test_migrating_v1_to_v2_with_pw_drops() -> Result<()> {
1116 let name = format!(
1117 "migrating-v2-with-cipher-drops-{}",
1118 Uuid::new_v4().as_hyphenated().to_string()
1119 );
1120 let passphrase = "some-other-passphrase".to_owned();
1121
1122 {
1124 let db = create_fake_db(&name, 1).await?;
1125 let tx = db.transaction(keys::CUSTOM).with_mode(TransactionMode::Readwrite).build()?;
1126 let custom = tx.object_store(keys::CUSTOM)?;
1127 let jskey = JsValue::from_str(
1128 core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1129 );
1130 custom.put(&serialize_value(None, &CUSTOM_DATA)?).with_key(jskey).await?;
1131 tx.commit().await?;
1132 db.close();
1133 }
1134
1135 let store = IndexeddbStateStore::builder()
1137 .name(name)
1138 .passphrase(passphrase)
1139 .migration_conflict_strategy(MigrationConflictStrategy::Drop)
1140 .build()
1141 .await?;
1142 assert_eq!(store.has_backups().await?, false);
1144 assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1146
1147 assert_eq!(store.version(), CURRENT_DB_VERSION);
1149 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1150
1151 Ok(())
1152 }
1153
1154 #[async_test]
1155 pub async fn test_migrating_v1_to_v2_with_pw_raise() -> Result<()> {
1156 let name = format!(
1157 "migrating-v2-with-cipher-raises-{}",
1158 Uuid::new_v4().as_hyphenated().to_string()
1159 );
1160 let passphrase = "some-other-passphrase".to_owned();
1161
1162 {
1164 let db = create_fake_db(&name, 1).await?;
1165 let tx = db.transaction(keys::CUSTOM).with_mode(TransactionMode::Readwrite).build()?;
1166 let custom = tx.object_store(keys::CUSTOM)?;
1167 let jskey = JsValue::from_str(
1168 core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1169 );
1170 custom.put(&serialize_value(None, &CUSTOM_DATA)?).with_key(jskey).await?;
1171 tx.commit().await?;
1172 db.close();
1173 }
1174
1175 let store_res = IndexeddbStateStore::builder()
1177 .name(name)
1178 .passphrase(passphrase)
1179 .migration_conflict_strategy(MigrationConflictStrategy::Raise)
1180 .build()
1181 .await;
1182
1183 assert_matches!(store_res, Err(IndexeddbStateStoreError::MigrationConflict { .. }));
1184
1185 Ok(())
1186 }
1187
1188 #[async_test]
1189 pub async fn test_migrating_to_v3() -> Result<()> {
1190 let name = format!("migrating-v3-{}", Uuid::new_v4().as_hyphenated().to_string());
1191
1192 let wrong_redacted_state_event = json!({
1194 "content": null,
1195 "event_id": "$wrongevent",
1196 "origin_server_ts": 1673887516047_u64,
1197 "sender": "@example:localhost",
1198 "state_key": "",
1199 "type": "m.room.topic",
1200 "unsigned": {
1201 "redacted_because": {
1202 "type": "m.room.redaction",
1203 "sender": "@example:localhost",
1204 "content": {},
1205 "redacts": "$wrongevent",
1206 "origin_server_ts": 1673893816047_u64,
1207 "unsigned": {},
1208 "event_id": "$redactionevent",
1209 },
1210 },
1211 });
1212 serde_json::from_value::<AnySyncStateEvent>(wrong_redacted_state_event.clone())
1213 .unwrap_err();
1214
1215 let room_id = room_id!("!some_room:localhost");
1216
1217 {
1219 let db = create_fake_db(&name, 2).await?;
1220 let tx =
1221 db.transaction(keys::ROOM_STATE).with_mode(TransactionMode::Readwrite).build()?;
1222 let state = tx.object_store(keys::ROOM_STATE)?;
1223 let key: JsValue = (room_id, StateEventType::RoomTopic, "").as_encoded_string().into();
1224 state.put(&serialize_value(None, &wrong_redacted_state_event)?).with_key(key).await?;
1225 tx.commit().await?;
1226 db.close();
1227 }
1228
1229 let store = IndexeddbStateStore::builder().name(name).build().await?;
1231 let event =
1232 store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap();
1233 event.deserialize().unwrap();
1234
1235 assert_eq!(store.version(), CURRENT_DB_VERSION);
1237 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1238
1239 Ok(())
1240 }
1241
1242 #[async_test]
1243 pub async fn test_migrating_to_v4() -> Result<()> {
1244 let name = format!("migrating-v4-{}", Uuid::new_v4().as_hyphenated().to_string());
1245
1246 let sync_token = "a_very_unique_string";
1247 let filter_1 = "filter_1";
1248 let filter_1_id = "filter_1_id";
1249 let filter_2 = "filter_2";
1250 let filter_2_id = "filter_2_id";
1251
1252 {
1254 let db = create_fake_db(&name, 3).await?;
1255 let tx = db
1256 .transaction([old_keys::SYNC_TOKEN, old_keys::SESSION])
1257 .with_mode(TransactionMode::Readwrite)
1258 .build()?;
1259
1260 let sync_token_store = tx.object_store(old_keys::SYNC_TOKEN)?;
1261 sync_token_store
1262 .put(&serialize_value(None, &sync_token)?)
1263 .with_key(JsValue::from_str(old_keys::SYNC_TOKEN))
1264 .build()?;
1265
1266 let session_store = tx.object_store(old_keys::SESSION)?;
1267 session_store
1268 .put(&serialize_value(None, &filter_1_id)?)
1269 .with_key(encode_key(
1270 None,
1271 StateStoreDataKey::FILTER,
1272 (StateStoreDataKey::FILTER, filter_1),
1273 ))
1274 .build()?;
1275 session_store
1276 .put(&serialize_value(None, &filter_2_id)?)
1277 .with_key(encode_key(
1278 None,
1279 StateStoreDataKey::FILTER,
1280 (StateStoreDataKey::FILTER, filter_2),
1281 ))
1282 .build()?;
1283
1284 tx.commit().await?;
1285 db.close();
1286 }
1287
1288 let store = IndexeddbStateStore::builder().name(name).build().await?;
1290
1291 let stored_sync_token = store
1292 .get_kv_data(StateStoreDataKey::SyncToken)
1293 .await?
1294 .unwrap()
1295 .into_sync_token()
1296 .unwrap();
1297 assert_eq!(stored_sync_token, sync_token);
1298
1299 let stored_filter_1_id = store
1300 .get_kv_data(StateStoreDataKey::Filter(filter_1))
1301 .await?
1302 .unwrap()
1303 .into_filter()
1304 .unwrap();
1305 assert_eq!(stored_filter_1_id, filter_1_id);
1306
1307 let stored_filter_2_id = store
1308 .get_kv_data(StateStoreDataKey::Filter(filter_2))
1309 .await?
1310 .unwrap()
1311 .into_filter()
1312 .unwrap();
1313 assert_eq!(stored_filter_2_id, filter_2_id);
1314
1315 Ok(())
1316 }
1317
1318 #[async_test]
1319 pub async fn test_migrating_to_v5() -> Result<()> {
1320 let name = format!("migrating-v5-{}", Uuid::new_v4().as_hyphenated().to_string());
1321
1322 let room_id = room_id!("!room:localhost");
1323 let user_id = user_id!("@invited:localhost");
1324 let f = EventFactory::new().room(room_id).sender(user_id!("@example:localhost"));
1325 let member_event: Raw<SyncRoomMemberEvent> = f
1326 .member(user_id)
1327 .invited(user_id)
1328 .display_name("example")
1329 .avatar_url(ruma::mxc_uri!("mxc://localhost/SEsfnsuifSDFSSEF"))
1330 .reason("Looking for support")
1331 .into();
1332
1333 let stripped_room_id = room_id!("!stripped_room:localhost");
1334 let stripped_user_id = user_id!("@example:localhost");
1335 let stripped_member_event: Raw<StrippedRoomMemberEvent> =
1336 f.member(stripped_user_id).display_name("example").into();
1337
1338 {
1340 let db = create_fake_db(&name, 4).await?;
1341 let tx = db
1342 .transaction([
1343 old_keys::MEMBERS,
1344 keys::ROOM_INFOS,
1345 old_keys::STRIPPED_MEMBERS,
1346 old_keys::STRIPPED_ROOM_INFOS,
1347 ])
1348 .with_mode(TransactionMode::Readwrite)
1349 .build()?;
1350
1351 let members_store = tx.object_store(old_keys::MEMBERS)?;
1352 members_store
1353 .put(&serialize_value(None, &member_event)?)
1354 .with_key(encode_key(None, old_keys::MEMBERS, (room_id, user_id)))
1355 .build()?;
1356 let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1357 let room_info = room_info_v1_json(room_id, RoomState::Joined, None, None);
1358 room_infos_store
1359 .put(&serialize_value(None, &room_info)?)
1360 .with_key(encode_key(None, keys::ROOM_INFOS, room_id))
1361 .build()?;
1362
1363 let stripped_members_store = tx.object_store(old_keys::STRIPPED_MEMBERS)?;
1364 stripped_members_store
1365 .put(&serialize_value(None, &stripped_member_event)?)
1366 .with_key(encode_key(
1367 None,
1368 old_keys::STRIPPED_MEMBERS,
1369 (stripped_room_id, stripped_user_id),
1370 ))
1371 .build()?;
1372 let stripped_room_infos_store = tx.object_store(old_keys::STRIPPED_ROOM_INFOS)?;
1373 let stripped_room_info =
1374 room_info_v1_json(stripped_room_id, RoomState::Invited, None, None);
1375 stripped_room_infos_store
1376 .put(&serialize_value(None, &stripped_room_info)?)
1377 .with_key(encode_key(None, old_keys::STRIPPED_ROOM_INFOS, stripped_room_id))
1378 .build()?;
1379
1380 tx.commit().await?;
1381 db.close();
1382 }
1383
1384 let store = IndexeddbStateStore::builder().name(name).build().await?;
1386
1387 assert_let!(
1388 Ok(Some(RawMemberEvent::Sync(stored_member_event))) =
1389 store.get_member_event(room_id, user_id).await
1390 );
1391 assert_eq!(stored_member_event.json().get(), member_event.json().get());
1392
1393 assert_let!(
1394 Ok(Some(RawMemberEvent::Stripped(stored_stripped_member_event))) =
1395 store.get_member_event(stripped_room_id, stripped_user_id).await
1396 );
1397 assert_eq!(stored_stripped_member_event.json().get(), stripped_member_event.json().get());
1398
1399 Ok(())
1400 }
1401
1402 #[async_test]
1403 pub async fn test_migrating_to_v6() -> Result<()> {
1404 let name = format!("migrating-v6-{}", Uuid::new_v4().as_hyphenated().to_string());
1405
1406 let room_id = room_id!("!room:localhost");
1407 let invite_user_id = user_id!("@invited:localhost");
1408 let ban_user_id = user_id!("@banned:localhost");
1409
1410 let f = EventFactory::new().room(room_id).sender(user_id!("@example:localhost"));
1411 let invite_member_event: Raw<SyncRoomMemberEvent> = f
1412 .member(invite_user_id)
1413 .invited(invite_user_id)
1414 .display_name("example")
1415 .avatar_url(ruma::mxc_uri!("mxc://localhost/SEsfnsuifSDFSSEF"))
1416 .reason("Looking for support")
1417 .into();
1418 let ban_member_event: Raw<SyncRoomMemberEvent> =
1419 f.member(ban_user_id).banned(ban_user_id).display_name("example").into();
1420
1421 let stripped_room_id = room_id!("!stripped_room:localhost");
1422 let stripped_user_id = user_id!("@example:localhost");
1423 let stripped_member_event: Raw<StrippedRoomMemberEvent> =
1424 f.member(stripped_user_id).display_name("example").into();
1425
1426 {
1428 let db = create_fake_db(&name, 5).await?;
1429 let tx = db
1430 .transaction([
1431 keys::ROOM_STATE,
1432 keys::ROOM_INFOS,
1433 keys::STRIPPED_ROOM_STATE,
1434 old_keys::STRIPPED_ROOM_INFOS,
1435 old_keys::INVITED_USER_IDS,
1436 old_keys::JOINED_USER_IDS,
1437 old_keys::STRIPPED_INVITED_USER_IDS,
1438 old_keys::STRIPPED_JOINED_USER_IDS,
1439 ])
1440 .with_mode(TransactionMode::Readwrite)
1441 .build()?;
1442
1443 let state_store = tx.object_store(keys::ROOM_STATE)?;
1444 state_store
1445 .put(&serialize_value(None, &invite_member_event)?)
1446 .with_key(encode_key(
1447 None,
1448 keys::ROOM_STATE,
1449 (room_id, StateEventType::RoomMember, invite_user_id),
1450 ))
1451 .build()?;
1452 state_store
1453 .put(&serialize_value(None, &ban_member_event)?)
1454 .with_key(encode_key(
1455 None,
1456 keys::ROOM_STATE,
1457 (room_id, StateEventType::RoomMember, ban_user_id),
1458 ))
1459 .build()?;
1460 let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1461 let room_info = room_info_v1_json(room_id, RoomState::Joined, None, None);
1462 room_infos_store
1463 .put(&serialize_value(None, &room_info)?)
1464 .with_key(encode_key(None, keys::ROOM_INFOS, room_id))
1465 .build()?;
1466
1467 let stripped_state_store = tx.object_store(keys::STRIPPED_ROOM_STATE)?;
1468 stripped_state_store
1469 .put(&serialize_value(None, &stripped_member_event)?)
1470 .with_key(encode_key(
1471 None,
1472 keys::STRIPPED_ROOM_STATE,
1473 (stripped_room_id, StateEventType::RoomMember, stripped_user_id),
1474 ))
1475 .build()?;
1476 let stripped_room_infos_store = tx.object_store(old_keys::STRIPPED_ROOM_INFOS)?;
1477 let stripped_room_info =
1478 room_info_v1_json(stripped_room_id, RoomState::Invited, None, None);
1479 stripped_room_infos_store
1480 .put(&serialize_value(None, &stripped_room_info)?)
1481 .with_key(encode_key(None, old_keys::STRIPPED_ROOM_INFOS, stripped_room_id))
1482 .build()?;
1483
1484 let joined_user_id = user_id!("@joined_user:localhost");
1486 tx.object_store(old_keys::JOINED_USER_IDS)?
1487 .put(&serialize_value(None, &joined_user_id)?)
1488 .with_key(encode_key(None, old_keys::JOINED_USER_IDS, (room_id, joined_user_id)))
1489 .build()?;
1490 let invited_user_id = user_id!("@invited_user:localhost");
1491 tx.object_store(old_keys::INVITED_USER_IDS)?
1492 .put(&serialize_value(None, &invited_user_id)?)
1493 .with_key(encode_key(None, old_keys::INVITED_USER_IDS, (room_id, invited_user_id)))
1494 .build()?;
1495 let stripped_joined_user_id = user_id!("@stripped_joined_user:localhost");
1496 tx.object_store(old_keys::STRIPPED_JOINED_USER_IDS)?
1497 .put(&serialize_value(None, &stripped_joined_user_id)?)
1498 .with_key(encode_key(
1499 None,
1500 old_keys::STRIPPED_JOINED_USER_IDS,
1501 (room_id, stripped_joined_user_id),
1502 ))
1503 .build()?;
1504 let stripped_invited_user_id = user_id!("@stripped_invited_user:localhost");
1505 tx.object_store(old_keys::STRIPPED_INVITED_USER_IDS)?
1506 .put(&serialize_value(None, &stripped_invited_user_id)?)
1507 .with_key(encode_key(
1508 None,
1509 old_keys::STRIPPED_INVITED_USER_IDS,
1510 (room_id, stripped_invited_user_id),
1511 ))
1512 .build()?;
1513
1514 tx.commit().await?;
1515 db.close();
1516 }
1517
1518 let store = IndexeddbStateStore::builder().name(name).build().await?;
1520
1521 assert_eq!(store.get_user_ids(room_id, RoomMemberships::JOIN).await.unwrap().len(), 0);
1522 assert_eq!(
1523 store.get_user_ids(room_id, RoomMemberships::INVITE).await.unwrap().as_slice(),
1524 [invite_user_id.to_owned()]
1525 );
1526 let user_ids = store.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
1527 assert_eq!(user_ids.len(), 2);
1528 assert!(user_ids.contains(&invite_user_id.to_owned()));
1529 assert!(user_ids.contains(&ban_user_id.to_owned()));
1530
1531 assert_eq!(
1532 store.get_user_ids(stripped_room_id, RoomMemberships::JOIN).await.unwrap().as_slice(),
1533 [stripped_user_id.to_owned()]
1534 );
1535 assert_eq!(
1536 store.get_user_ids(stripped_room_id, RoomMemberships::INVITE).await.unwrap().len(),
1537 0
1538 );
1539 assert_eq!(
1540 store
1541 .get_user_ids(stripped_room_id, RoomMemberships::empty())
1542 .await
1543 .unwrap()
1544 .as_slice(),
1545 [stripped_user_id.to_owned()]
1546 );
1547
1548 Ok(())
1549 }
1550
1551 #[async_test]
1552 pub async fn test_migrating_to_v7() -> Result<()> {
1553 let name = format!("migrating-v7-{}", Uuid::new_v4().as_hyphenated().to_string());
1554
1555 let room_id = room_id!("!room:localhost");
1556 let stripped_room_id = room_id!("!stripped_room:localhost");
1557
1558 {
1560 let db = create_fake_db(&name, 6).await?;
1561 let tx = db
1562 .transaction([keys::ROOM_INFOS, old_keys::STRIPPED_ROOM_INFOS])
1563 .with_mode(TransactionMode::Readwrite)
1564 .build()?;
1565
1566 let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1567 let room_info = room_info_v1_json(room_id, RoomState::Joined, None, None);
1568 room_infos_store
1569 .put(&serialize_value(None, &room_info)?)
1570 .with_key(encode_key(None, keys::ROOM_INFOS, room_id))
1571 .build()?;
1572
1573 let stripped_room_infos_store = tx.object_store(old_keys::STRIPPED_ROOM_INFOS)?;
1574 let stripped_room_info =
1575 room_info_v1_json(stripped_room_id, RoomState::Invited, None, None);
1576 stripped_room_infos_store
1577 .put(&serialize_value(None, &stripped_room_info)?)
1578 .with_key(encode_key(None, old_keys::STRIPPED_ROOM_INFOS, stripped_room_id))
1579 .build()?;
1580
1581 tx.commit().await?;
1582 db.close();
1583 }
1584
1585 let store = IndexeddbStateStore::builder().name(name).build().await?;
1587
1588 assert_eq!(store.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 2);
1589
1590 Ok(())
1591 }
1592
1593 fn add_room_v7(
1595 room_infos_store: &ObjectStore<'_>,
1596 room_state_store: &ObjectStore<'_>,
1597 room_id: &RoomId,
1598 name: Option<&str>,
1599 create_creator: Option<OwnedUserId>,
1600 create_sender: Option<&UserId>,
1601 ) -> Result<()> {
1602 let room_info_json =
1603 room_info_v1_json(room_id, RoomState::Joined, name, create_creator.as_deref());
1604
1605 room_infos_store
1606 .put(&serialize_value(None, &room_info_json)?)
1607 .with_key(encode_key(None, keys::ROOM_INFOS, room_id))
1608 .build()?;
1609
1610 let Some(create_sender) = create_sender else {
1612 return Ok(());
1613 };
1614
1615 let create_content = match create_creator {
1616 Some(creator) => RoomCreateEventContent::new_v1(creator),
1617 None => RoomCreateEventContent::new_v11(),
1618 };
1619
1620 let event_id = EventId::new_v1(server_name!("dummy.local"));
1621 let create_event = json!({
1622 "content": create_content,
1623 "event_id": event_id,
1624 "sender": create_sender,
1625 "origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
1626 "state_key": "",
1627 "type": "m.room.create",
1628 "unsigned": {},
1629 });
1630
1631 room_state_store
1632 .put(&serialize_value(None, &create_event)?)
1633 .with_key(encode_key(
1634 None,
1635 keys::ROOM_STATE,
1636 (room_id, &StateEventType::RoomCreate, ""),
1637 ))
1638 .build()?;
1639
1640 Ok(())
1641 }
1642
1643 #[async_test]
1644 pub async fn test_migrating_to_v8() -> Result<()> {
1645 let name = format!("migrating-v8-{}", Uuid::new_v4().as_hyphenated().to_string());
1646
1647 let room_a_id = room_id!("!room_a:dummy.local");
1649 let room_a_name = "Room A";
1650 let room_a_creator = owned_user_id!("@creator:dummy.local");
1651 let room_a_create_sender = owned_user_id!("@sender:dummy.local");
1654
1655 let room_b_id = room_id!("!room_b:dummy.local");
1657
1658 let room_c_id = room_id!("!room_c:dummy.local");
1660 let room_c_create_sender = owned_user_id!("@creator:dummy.local");
1661
1662 {
1664 let db = create_fake_db(&name, 6).await?;
1665 let tx = db
1666 .transaction([keys::ROOM_INFOS, keys::ROOM_STATE])
1667 .with_mode(TransactionMode::Readwrite)
1668 .build()?;
1669
1670 let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1671 let room_state_store = tx.object_store(keys::ROOM_STATE)?;
1672
1673 add_room_v7(
1674 &room_infos_store,
1675 &room_state_store,
1676 room_a_id,
1677 Some(room_a_name),
1678 Some(room_a_creator),
1679 Some(&room_a_create_sender),
1680 )?;
1681 add_room_v7(&room_infos_store, &room_state_store, room_b_id, None, None, None)?;
1682 add_room_v7(
1683 &room_infos_store,
1684 &room_state_store,
1685 room_c_id,
1686 None,
1687 None,
1688 Some(&room_c_create_sender),
1689 )?;
1690
1691 tx.commit().await?;
1692 db.close();
1693 }
1694
1695 let store = IndexeddbStateStore::builder().name(name).build().await?;
1697
1698 let room_infos = store.get_room_infos(&RoomLoadSettings::default()).await?;
1700 assert_eq!(room_infos.len(), 3);
1701
1702 let room_a = room_infos.iter().find(|r| r.room_id() == room_a_id).unwrap();
1703 assert_eq!(room_a.name(), Some(room_a_name));
1704 assert_eq!(room_a.creators(), Some(vec![room_a_create_sender]));
1705
1706 let room_b = room_infos.iter().find(|r| r.room_id() == room_b_id).unwrap();
1707 assert_eq!(room_b.name(), None);
1708 assert_eq!(room_b.creators(), None);
1709
1710 let room_c = room_infos.iter().find(|r| r.room_id() == room_c_id).unwrap();
1711 assert_eq!(room_c.name(), None);
1712 assert_eq!(room_c.creators(), Some(vec![room_c_create_sender]));
1713
1714 Ok(())
1715 }
1716}