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