LCOV - code coverage report
Current view: top level - lib/src/database - hive_database.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 479 690 69.4 %
Date: 2024-09-27 11:38:01 Functions: 0 0 -

          Line data    Source code
       1             : /*
       2             :  *   Famedly Matrix SDK
       3             :  *   Copyright (C) 2019, 2020, 2021 Famedly GmbH
       4             :  *
       5             :  *   This program is free software: you can redistribute it and/or modify
       6             :  *   it under the terms of the GNU Affero General Public License as
       7             :  *   published by the Free Software Foundation, either version 3 of the
       8             :  *   License, or (at your option) any later version.
       9             :  *
      10             :  *   This program is distributed in the hope that it will be useful,
      11             :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12             :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13             :  *   GNU Affero General Public License for more details.
      14             :  *
      15             :  *   You should have received a copy of the GNU Affero General Public License
      16             :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17             :  */
      18             : 
      19             : import 'dart:async';
      20             : import 'dart:convert';
      21             : import 'dart:math';
      22             : import 'dart:typed_data';
      23             : 
      24             : import 'package:hive/hive.dart';
      25             : 
      26             : import 'package:matrix/encryption/utils/olm_session.dart';
      27             : import 'package:matrix/encryption/utils/outbound_group_session.dart';
      28             : import 'package:matrix/encryption/utils/ssss_cache.dart';
      29             : import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
      30             : import 'package:matrix/matrix.dart';
      31             : import 'package:matrix/src/database/zone_transaction_mixin.dart';
      32             : import 'package:matrix/src/utils/copy_map.dart';
      33             : import 'package:matrix/src/utils/queued_to_device_event.dart';
      34             : import 'package:matrix/src/utils/run_benchmarked.dart';
      35             : 
      36             : /// This is a basic database for the Matrix SDK using the hive store. You need
      37             : /// to make sure that you perform `Hive.init()` or `Hive.flutterInit()` before
      38             : /// you use this.
      39             : ///
      40             : /// This database does not support file caching!
      41             : @Deprecated(
      42             :     'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!')
      43             : class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
      44             :   static const int version = 6;
      45             :   final String name;
      46             :   late Box _clientBox;
      47             :   late Box _accountDataBox;
      48             :   late Box _roomsBox;
      49             :   late Box _toDeviceQueueBox;
      50             : 
      51             :   /// Key is a tuple as MultiKey(roomId, type) where stateKey can be
      52             :   /// an empty string.
      53             :   late LazyBox _roomStateBox;
      54             : 
      55             :   /// Key is a tuple as MultiKey(roomId, userId)
      56             :   late LazyBox _roomMembersBox;
      57             : 
      58             :   /// Key is a tuple as MultiKey(roomId, type)
      59             :   late LazyBox _roomAccountDataBox;
      60             :   late LazyBox _inboundGroupSessionsBox;
      61             :   late LazyBox _outboundGroupSessionsBox;
      62             :   late LazyBox _olmSessionsBox;
      63             : 
      64             :   /// Key is a tuple as MultiKey(userId, deviceId)
      65             :   late LazyBox _userDeviceKeysBox;
      66             : 
      67             :   /// Key is the user ID as a String
      68             :   late LazyBox _userDeviceKeysOutdatedBox;
      69             : 
      70             :   /// Key is a tuple as MultiKey(userId, publicKey)
      71             :   late LazyBox _userCrossSigningKeysBox;
      72             :   late LazyBox _ssssCacheBox;
      73             :   late LazyBox _presencesBox;
      74             : 
      75             :   /// Key is a tuple as Multikey(roomId, fragmentId) while the default
      76             :   /// fragmentId is an empty String
      77             :   late LazyBox _timelineFragmentsBox;
      78             : 
      79             :   /// Key is a tuple as MultiKey(roomId, eventId)
      80             :   late LazyBox _eventsBox;
      81             : 
      82             :   /// Key is a tuple as MultiKey(userId, deviceId)
      83             :   late LazyBox _seenDeviceIdsBox;
      84             : 
      85             :   late LazyBox _seenDeviceKeysBox;
      86             : 
      87           3 :   String get _clientBoxName => '$name.box.client';
      88             : 
      89           3 :   String get _accountDataBoxName => '$name.box.account_data';
      90             : 
      91           3 :   String get _roomsBoxName => '$name.box.rooms';
      92             : 
      93           3 :   String get _toDeviceQueueBoxName => '$name.box.to_device_queue';
      94             : 
      95           3 :   String get _roomStateBoxName => '$name.box.room_states';
      96             : 
      97           3 :   String get _roomMembersBoxName => '$name.box.room_members';
      98             : 
      99           3 :   String get _roomAccountDataBoxName => '$name.box.room_account_data';
     100             : 
     101           3 :   String get _inboundGroupSessionsBoxName => '$name.box.inbound_group_session';
     102             : 
     103           1 :   String get _outboundGroupSessionsBoxName =>
     104           2 :       '$name.box.outbound_group_session';
     105             : 
     106           3 :   String get _olmSessionsBoxName => '$name.box.olm_session';
     107             : 
     108           3 :   String get _userDeviceKeysBoxName => '$name.box.user_device_keys';
     109             : 
     110           1 :   String get _userDeviceKeysOutdatedBoxName =>
     111           2 :       '$name.box.user_device_keys_outdated';
     112             : 
     113           3 :   String get _userCrossSigningKeysBoxName => '$name.box.cross_signing_keys';
     114             : 
     115           3 :   String get _ssssCacheBoxName => '$name.box.ssss_cache';
     116             : 
     117           3 :   String get _presencesBoxName => '$name.box.presences';
     118             : 
     119           3 :   String get _timelineFragmentsBoxName => '$name.box.timeline_fragments';
     120             : 
     121           3 :   String get _eventsBoxName => '$name.box.events';
     122             : 
     123           3 :   String get _seenDeviceIdsBoxName => '$name.box.seen_device_ids';
     124             : 
     125           3 :   String get _seenDeviceKeysBoxName => '$name.box.seen_device_keys';
     126             : 
     127             :   final HiveCipher? encryptionCipher;
     128             : 
     129           1 :   FamedlySdkHiveDatabase(this.name, {this.encryptionCipher});
     130             : 
     131           0 :   @override
     132             :   int get maxFileSize => 0;
     133             : 
     134           1 :   Future<void> _actionOnAllBoxes(Future<void> Function(BoxBase box) action) =>
     135           2 :       Future.wait([
     136           2 :         action(_clientBox),
     137           2 :         action(_accountDataBox),
     138           2 :         action(_roomsBox),
     139           2 :         action(_roomStateBox),
     140           2 :         action(_roomMembersBox),
     141           2 :         action(_toDeviceQueueBox),
     142           2 :         action(_roomAccountDataBox),
     143           2 :         action(_inboundGroupSessionsBox),
     144           2 :         action(_outboundGroupSessionsBox),
     145           2 :         action(_olmSessionsBox),
     146           2 :         action(_userDeviceKeysBox),
     147           2 :         action(_userDeviceKeysOutdatedBox),
     148           2 :         action(_userCrossSigningKeysBox),
     149           2 :         action(_ssssCacheBox),
     150           2 :         action(_presencesBox),
     151           2 :         action(_timelineFragmentsBox),
     152           2 :         action(_eventsBox),
     153           2 :         action(_seenDeviceIdsBox),
     154           2 :         action(_seenDeviceKeysBox),
     155             :       ]);
     156             : 
     157           1 :   Future<void> open() async {
     158           3 :     _clientBox = await Hive.openBox(
     159           1 :       _clientBoxName,
     160           1 :       encryptionCipher: encryptionCipher,
     161             :     );
     162           3 :     _accountDataBox = await Hive.openBox(
     163           1 :       _accountDataBoxName,
     164           1 :       encryptionCipher: encryptionCipher,
     165             :     );
     166           3 :     _roomsBox = await Hive.openBox(
     167           1 :       _roomsBoxName,
     168           1 :       encryptionCipher: encryptionCipher,
     169             :     );
     170           3 :     _roomStateBox = await Hive.openLazyBox(
     171           1 :       _roomStateBoxName,
     172           1 :       encryptionCipher: encryptionCipher,
     173             :     );
     174           3 :     _roomMembersBox = await Hive.openLazyBox(
     175           1 :       _roomMembersBoxName,
     176           1 :       encryptionCipher: encryptionCipher,
     177             :     );
     178           3 :     _toDeviceQueueBox = await Hive.openBox(
     179           1 :       _toDeviceQueueBoxName,
     180           1 :       encryptionCipher: encryptionCipher,
     181             :     );
     182           3 :     _roomAccountDataBox = await Hive.openLazyBox(
     183           1 :       _roomAccountDataBoxName,
     184           1 :       encryptionCipher: encryptionCipher,
     185             :     );
     186           3 :     _inboundGroupSessionsBox = await Hive.openLazyBox(
     187           1 :       _inboundGroupSessionsBoxName,
     188           1 :       encryptionCipher: encryptionCipher,
     189             :     );
     190           3 :     _outboundGroupSessionsBox = await Hive.openLazyBox(
     191           1 :       _outboundGroupSessionsBoxName,
     192           1 :       encryptionCipher: encryptionCipher,
     193             :     );
     194           3 :     _olmSessionsBox = await Hive.openLazyBox(
     195           1 :       _olmSessionsBoxName,
     196           1 :       encryptionCipher: encryptionCipher,
     197             :     );
     198           3 :     _userDeviceKeysBox = await Hive.openLazyBox(
     199           1 :       _userDeviceKeysBoxName,
     200           1 :       encryptionCipher: encryptionCipher,
     201             :     );
     202           3 :     _userDeviceKeysOutdatedBox = await Hive.openLazyBox(
     203           1 :       _userDeviceKeysOutdatedBoxName,
     204           1 :       encryptionCipher: encryptionCipher,
     205             :     );
     206           3 :     _userCrossSigningKeysBox = await Hive.openLazyBox(
     207           1 :       _userCrossSigningKeysBoxName,
     208           1 :       encryptionCipher: encryptionCipher,
     209             :     );
     210           3 :     _ssssCacheBox = await Hive.openLazyBox(
     211           1 :       _ssssCacheBoxName,
     212           1 :       encryptionCipher: encryptionCipher,
     213             :     );
     214           3 :     _presencesBox = await Hive.openLazyBox(
     215           1 :       _presencesBoxName,
     216           1 :       encryptionCipher: encryptionCipher,
     217             :     );
     218           3 :     _timelineFragmentsBox = await Hive.openLazyBox(
     219           1 :       _timelineFragmentsBoxName,
     220           1 :       encryptionCipher: encryptionCipher,
     221             :     );
     222           3 :     _eventsBox = await Hive.openLazyBox(
     223           1 :       _eventsBoxName,
     224           1 :       encryptionCipher: encryptionCipher,
     225             :     );
     226           3 :     _seenDeviceIdsBox = await Hive.openLazyBox(
     227           1 :       _seenDeviceIdsBoxName,
     228           1 :       encryptionCipher: encryptionCipher,
     229             :     );
     230           3 :     _seenDeviceKeysBox = await Hive.openLazyBox(
     231           1 :       _seenDeviceKeysBoxName,
     232           1 :       encryptionCipher: encryptionCipher,
     233             :     );
     234             : 
     235             :     // Check version and check if we need a migration
     236           2 :     final currentVersion = (await _clientBox.get('version') as int?);
     237             :     if (currentVersion == null) {
     238           2 :       await _clientBox.put('version', version);
     239           0 :     } else if (currentVersion != version) {
     240           0 :       await _migrateFromVersion(currentVersion);
     241             :     }
     242             : 
     243             :     return;
     244             :   }
     245             : 
     246           0 :   Future<void> _migrateFromVersion(int currentVersion) async {
     247           0 :     Logs().i('Migrate Hive database from version $currentVersion to $version');
     248           0 :     if (version == 5) {
     249           0 :       for (final key in _userDeviceKeysBox.keys) {
     250             :         try {
     251           0 :           final raw = await _userDeviceKeysBox.get(key) as Map;
     252           0 :           if (!raw.containsKey('keys')) continue;
     253           0 :           final deviceKeys = DeviceKeys.fromJson(
     254           0 :             convertToJson(raw),
     255           0 :             Client(''),
     256             :           );
     257           0 :           await addSeenDeviceId(deviceKeys.userId, deviceKeys.deviceId!,
     258           0 :               deviceKeys.curve25519Key! + deviceKeys.ed25519Key!);
     259           0 :           await addSeenPublicKey(deviceKeys.ed25519Key!, deviceKeys.deviceId!);
     260           0 :           await addSeenPublicKey(
     261           0 :               deviceKeys.curve25519Key!, deviceKeys.deviceId!);
     262             :         } catch (e) {
     263           0 :           Logs().w('Can not migrate device $key', e);
     264             :         }
     265             :       }
     266             :     }
     267           0 :     await clearCache();
     268           0 :     await _clientBox.put('version', version);
     269             :   }
     270             : 
     271           1 :   @override
     272             :   Future<void> clear() async {
     273           2 :     Logs().i('Clear and close hive database...');
     274           2 :     await _actionOnAllBoxes((box) async {
     275             :       try {
     276           2 :         await box.deleteAll(box.keys);
     277           1 :         await box.close();
     278             :       } catch (e) {
     279           0 :         Logs().v('Unable to clear box ${box.name}', e);
     280           0 :         await box.deleteFromDisk();
     281             :       }
     282             :     });
     283             :     return;
     284             :   }
     285             : 
     286           1 :   @override
     287             :   Future<void> clearCache() async {
     288           4 :     await _roomsBox.deleteAll(_roomsBox.keys);
     289           4 :     await _accountDataBox.deleteAll(_accountDataBox.keys);
     290           4 :     await _roomAccountDataBox.deleteAll(_roomAccountDataBox.keys);
     291           4 :     await _roomStateBox.deleteAll(_roomStateBox.keys);
     292           4 :     await _roomMembersBox.deleteAll(_roomMembersBox.keys);
     293           4 :     await _eventsBox.deleteAll(_eventsBox.keys);
     294           4 :     await _timelineFragmentsBox.deleteAll(_timelineFragmentsBox.keys);
     295           4 :     await _outboundGroupSessionsBox.deleteAll(_outboundGroupSessionsBox.keys);
     296           4 :     await _presencesBox.deleteAll(_presencesBox.keys);
     297           2 :     await _clientBox.delete('prev_batch');
     298             :   }
     299             : 
     300           1 :   @override
     301             :   Future<void> clearSSSSCache() async {
     302           4 :     await _ssssCacheBox.deleteAll(_ssssCacheBox.keys);
     303             :   }
     304             : 
     305           1 :   @override
     306           3 :   Future<void> close() => _actionOnAllBoxes((box) => box.close());
     307             : 
     308           1 :   @override
     309             :   Future<void> deleteFromToDeviceQueue(int id) async {
     310           2 :     await _toDeviceQueueBox.delete(id);
     311             :     return;
     312             :   }
     313             : 
     314           1 :   @override
     315             :   Future<void> deleteOldFiles(int savedAt) async {
     316             :     return;
     317             :   }
     318             : 
     319           1 :   @override
     320             :   Future<void> forgetRoom(String roomId) async {
     321           4 :     await _timelineFragmentsBox.delete(MultiKey(roomId, '').toString());
     322           2 :     for (final key in _eventsBox.keys) {
     323           0 :       final multiKey = MultiKey.fromString(key);
     324           0 :       if (multiKey.parts.first != roomId) continue;
     325           0 :       await _eventsBox.delete(key);
     326             :     }
     327           2 :     for (final key in _roomStateBox.keys) {
     328           0 :       final multiKey = MultiKey.fromString(key);
     329           0 :       if (multiKey.parts.first != roomId) continue;
     330           0 :       await _roomStateBox.delete(key);
     331             :     }
     332           2 :     for (final key in _roomMembersBox.keys) {
     333           0 :       final multiKey = MultiKey.fromString(key);
     334           0 :       if (multiKey.parts.first != roomId) continue;
     335           0 :       await _roomMembersBox.delete(key);
     336             :     }
     337           2 :     for (final key in _roomAccountDataBox.keys) {
     338           0 :       final multiKey = MultiKey.fromString(key);
     339           0 :       if (multiKey.parts.first != roomId) continue;
     340           0 :       await _roomAccountDataBox.delete(key);
     341             :     }
     342           3 :     await _roomsBox.delete(roomId.toHiveKey);
     343             :   }
     344             : 
     345           1 :   @override
     346             :   Future<Map<String, BasicEvent>> getAccountData() =>
     347           1 :       runBenchmarked<Map<String, BasicEvent>>('Get all account data from Hive',
     348           1 :           () async {
     349           1 :         final accountData = <String, BasicEvent>{};
     350           3 :         for (final key in _accountDataBox.keys) {
     351           2 :           final raw = await _accountDataBox.get(key);
     352           4 :           accountData[key.toString().fromHiveKey] = BasicEvent(
     353           2 :             type: key.toString().fromHiveKey,
     354           1 :             content: convertToJson(raw),
     355             :           );
     356             :         }
     357             :         return accountData;
     358           3 :       }, _accountDataBox.keys.length);
     359             : 
     360           1 :   @override
     361             :   Future<Map<String, dynamic>?> getClient(String name) =>
     362           2 :       runBenchmarked('Get Client from Hive', () async {
     363           1 :         final map = <String, dynamic>{};
     364           3 :         for (final key in _clientBox.keys) {
     365           1 :           if (key == 'version') continue;
     366           3 :           map[key] = await _clientBox.get(key);
     367             :         }
     368           1 :         if (map.isEmpty) return null;
     369             :         return map;
     370             :       });
     371             : 
     372           1 :   @override
     373             :   Future<Event?> getEventById(String eventId, Room room) async {
     374           5 :     final raw = await _eventsBox.get(MultiKey(room.id, eventId).toString());
     375             :     if (raw == null) return null;
     376           2 :     return Event.fromJson(convertToJson(raw), room);
     377             :   }
     378             : 
     379             :   /// Loads a whole list of events at once from the store for a specific room
     380           1 :   Future<List<Event>> _getEventsByIds(List<String> eventIds, Room room) async {
     381           3 :     final events = await Future.wait(eventIds.map((String eventId) async {
     382           5 :       final entry = await _eventsBox.get(MultiKey(room.id, eventId).toString());
     383           3 :       return entry is Map ? Event.fromJson(convertToJson(entry), room) : null;
     384             :     }));
     385             : 
     386           2 :     return events.whereType<Event>().toList();
     387             :   }
     388             : 
     389           1 :   @override
     390             :   Future<List<Event>> getEventList(
     391             :     Room room, {
     392             :     int start = 0,
     393             :     bool onlySending = false,
     394             :     int? limit,
     395             :   }) =>
     396           2 :       runBenchmarked<List<Event>>('Get event list', () async {
     397             :         // Get the synced event IDs from the store
     398           3 :         final timelineKey = MultiKey(room.id, '').toString();
     399           1 :         final timelineEventIds = List<String>.from(
     400           2 :             (await _timelineFragmentsBox.get(timelineKey)) ?? []);
     401             :         // Get the local stored SENDING events from the store
     402             :         late final List sendingEventIds;
     403           1 :         if (start != 0) {
     404           0 :           sendingEventIds = [];
     405             :         } else {
     406           3 :           final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString();
     407           1 :           sendingEventIds = List<String>.from(
     408           3 :               (await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
     409             :         }
     410             : 
     411             :         // Combine those two lists while respecting the start and limit parameters.
     412           1 :         final end = min(timelineEventIds.length,
     413           2 :             start + (limit ?? timelineEventIds.length));
     414           1 :         final eventIds = List<String>.from(
     415           1 :           [
     416             :             ...sendingEventIds,
     417           2 :             ...(start < timelineEventIds.length && !onlySending
     418           3 :                 ? timelineEventIds.getRange(start, end).toList()
     419           0 :                 : [])
     420             :           ],
     421             :         );
     422             : 
     423           1 :         return await _getEventsByIds(eventIds, room);
     424             :       });
     425             : 
     426           0 :   @override
     427             :   Future<List<String>> getEventIdList(
     428             :     Room room, {
     429             :     int start = 0,
     430             :     bool includeSending = false,
     431             :     int? limit,
     432             :   }) =>
     433           0 :       runBenchmarked<List<String>>('Get event id list', () async {
     434             :         // Get the synced event IDs from the store
     435           0 :         final timelineKey = MultiKey(room.id, '').toString();
     436             : 
     437           0 :         final timelineEventIds = List<String>.from(
     438           0 :             (await _timelineFragmentsBox.get(timelineKey)) ?? []);
     439             : 
     440             :         // Get the local stored SENDING events from the store
     441             :         late final List<String> sendingEventIds;
     442             :         if (!includeSending) {
     443           0 :           sendingEventIds = [];
     444             :         } else {
     445           0 :           final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString();
     446           0 :           sendingEventIds = List<String>.from(
     447           0 :               (await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
     448             :         }
     449             : 
     450             :         // Combine those two lists while respecting the start and limit parameters.
     451           0 :         final eventIds = sendingEventIds + timelineEventIds;
     452           0 :         if (limit != null && eventIds.length > limit) {
     453           0 :           eventIds.removeRange(limit, eventIds.length);
     454             :         }
     455             : 
     456             :         return eventIds;
     457             :       });
     458             : 
     459           1 :   @override
     460             :   Future<Uint8List?> getFile(Uri mxcUri) async {
     461             :     return null;
     462             :   }
     463             : 
     464           1 :   @override
     465             :   Future<StoredInboundGroupSession?> getInboundGroupSession(
     466             :     String roomId,
     467             :     String sessionId,
     468             :   ) async {
     469           3 :     final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
     470             :     if (raw == null) return null;
     471           2 :     return StoredInboundGroupSession.fromJson(convertToJson(raw));
     472             :   }
     473             : 
     474           1 :   @override
     475             :   Future<List<StoredInboundGroupSession>>
     476             :       getInboundGroupSessionsToUpload() async {
     477           4 :     final sessions = (await Future.wait(_inboundGroupSessionsBox.keys.map(
     478           0 :             (sessionId) async =>
     479           0 :                 await _inboundGroupSessionsBox.get(sessionId))))
     480           1 :         .where((rawSession) => rawSession['uploaded'] == false)
     481           1 :         .take(500)
     482           1 :         .map(
     483           0 :           (json) => StoredInboundGroupSession.fromJson(
     484           0 :             convertToJson(json),
     485             :           ),
     486             :         )
     487           1 :         .toList();
     488             :     return sessions;
     489             :   }
     490             : 
     491           1 :   @override
     492             :   Future<List<String>> getLastSentMessageUserDeviceKey(
     493             :       String userId, String deviceId) async {
     494             :     final raw =
     495           4 :         await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
     496           1 :     if (raw == null) return <String>[];
     497           0 :     return <String>[raw['last_sent_message']];
     498             :   }
     499             : 
     500           1 :   @override
     501             :   Future<void> storeOlmSession(String identityKey, String sessionId,
     502             :       String pickle, int lastReceived) async {
     503             :     final rawSessions =
     504           4 :         (await _olmSessionsBox.get(identityKey.toHiveKey) as Map?) ?? {};
     505           2 :     rawSessions[sessionId] = <String, dynamic>{
     506             :       'identity_key': identityKey,
     507             :       'pickle': pickle,
     508             :       'session_id': sessionId,
     509             :       'last_received': lastReceived,
     510             :     };
     511           3 :     await _olmSessionsBox.put(identityKey.toHiveKey, rawSessions);
     512             :     return;
     513             :   }
     514             : 
     515           1 :   @override
     516             :   Future<List<OlmSession>> getOlmSessions(
     517             :       String identityKey, String userId) async {
     518             :     final rawSessions =
     519           4 :         await _olmSessionsBox.get(identityKey.toHiveKey) as Map? ?? {};
     520             : 
     521           1 :     return rawSessions.values
     522           4 :         .map((json) => OlmSession.fromJson(convertToJson(json), userId))
     523           1 :         .toList();
     524             :   }
     525             : 
     526           1 :   @override
     527             :   Future<Map<String, Map>> getAllOlmSessions() async {
     528           1 :     final backup = Map.fromEntries(
     529           1 :       await Future.wait(
     530           3 :         _olmSessionsBox.keys.map(
     531           2 :           (key) async => MapEntry(
     532             :             key,
     533           2 :             await _olmSessionsBox.get(key),
     534             :           ),
     535             :         ),
     536             :       ),
     537             :     );
     538           1 :     return backup.cast<String, Map>();
     539             :   }
     540             : 
     541           1 :   @override
     542             :   Future<List<OlmSession>> getOlmSessionsForDevices(
     543             :       List<String> identityKeys, String userId) async {
     544           1 :     final sessions = await Future.wait(
     545           3 :         identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)));
     546           3 :     return <OlmSession>[for (final sublist in sessions) ...sublist];
     547             :   }
     548             : 
     549           1 :   @override
     550             :   Future<OutboundGroupSession?> getOutboundGroupSession(
     551             :       String roomId, String userId) async {
     552           3 :     final raw = await _outboundGroupSessionsBox.get(roomId.toHiveKey);
     553             :     if (raw == null) return null;
     554           2 :     return OutboundGroupSession.fromJson(convertToJson(raw), userId);
     555             :   }
     556             : 
     557           1 :   @override
     558             :   Future<Room?> getSingleRoom(Client client, String roomId,
     559             :       {bool loadImportantStates = true}) async {
     560             :     // Get raw room from database:
     561           2 :     final roomData = await _roomsBox.get(roomId);
     562             :     if (roomData == null) return null;
     563           2 :     final room = Room.fromJson(convertToJson(roomData), client);
     564             : 
     565             :     // Get important states:
     566             :     if (loadImportantStates) {
     567           1 :       final dbKeys = client.importantStateEvents
     568           4 :           .map((state) => TupleKey(roomId, state).toString())
     569           1 :           .toList();
     570           1 :       final rawStates = await Future.wait(
     571           4 :         dbKeys.map((key) => _roomStateBox.get(key)),
     572             :       );
     573           2 :       for (final rawState in rawStates) {
     574           1 :         if (rawState == null || rawState[''] == null) continue;
     575           4 :         room.setState(Event.fromJson(convertToJson(rawState['']), room));
     576             :       }
     577             :     }
     578             : 
     579             :     return room;
     580             :   }
     581             : 
     582           1 :   @override
     583             :   Future<List<Room>> getRoomList(Client client) =>
     584           2 :       runBenchmarked<List<Room>>('Get room list from hive', () async {
     585           1 :         final rooms = <String, Room>{};
     586           1 :         final userID = client.userID;
     587           1 :         final importantRoomStates = client.importantStateEvents;
     588           3 :         for (final key in _roomsBox.keys) {
     589             :           // Get the room
     590           2 :           final raw = await _roomsBox.get(key);
     591           2 :           final room = Room.fromJson(convertToJson(raw), client);
     592             : 
     593             :           // let's see if we need any m.room.member events
     594             :           // We always need the member event for ourself
     595           0 :           final membersToPostload = <String>{if (userID != null) userID};
     596             :           // If the room is a direct chat, those IDs should be there too
     597           1 :           if (room.isDirectChat) {
     598           0 :             membersToPostload.add(room.directChatMatrixID!);
     599             :           }
     600             :           // the lastEvent message preview might have an author we need to fetch, if it is a group chat
     601           1 :           final lastEvent = room.getState(EventTypes.Message);
     602           0 :           if (lastEvent != null && !room.isDirectChat) {
     603           0 :             membersToPostload.add(lastEvent.senderId);
     604             :           }
     605             :           // if the room has no name and no canonical alias, its name is calculated
     606             :           // based on the heroes of the room
     607           1 :           if (room.getState(EventTypes.RoomName) == null &&
     608           1 :               room.getState(EventTypes.RoomCanonicalAlias) == null) {
     609             :             // we don't have a name and no canonical alias, so we'll need to
     610             :             // post-load the heroes
     611           3 :             membersToPostload.addAll(room.summary.mHeroes ?? []);
     612             :           }
     613             :           // Load members
     614           1 :           for (final userId in membersToPostload) {
     615             :             final state =
     616           0 :                 await _roomMembersBox.get(MultiKey(room.id, userId).toString());
     617             :             if (state == null) {
     618           0 :               Logs().w('Unable to post load member $userId');
     619             :               continue;
     620             :             }
     621           0 :             room.setState(room.membership == Membership.invite
     622           0 :                 ? StrippedStateEvent.fromJson(copyMap(raw))
     623           0 :                 : Event.fromJson(convertToJson(state), room));
     624             :           }
     625             : 
     626             :           // Get the "important" room states. All other states will be loaded once
     627             :           // `getUnimportantRoomStates()` is called.
     628           2 :           for (final type in importantRoomStates) {
     629           1 :             final states = await _roomStateBox
     630           4 :                 .get(MultiKey(room.id, type).toString()) as Map?;
     631             :             if (states == null) continue;
     632           0 :             final stateEvents = states.values
     633           0 :                 .map((raw) => room.membership == Membership.invite
     634           0 :                     ? StrippedStateEvent.fromJson(copyMap(raw))
     635           0 :                     : Event.fromJson(convertToJson(raw), room))
     636           0 :                 .toList();
     637           0 :             for (final state in stateEvents) {
     638           0 :               room.setState(state);
     639             :             }
     640             :           }
     641             : 
     642             :           // Add to the list and continue.
     643           2 :           rooms[room.id] = room;
     644             :         }
     645             : 
     646             :         // Get the room account data
     647           2 :         for (final key in _roomAccountDataBox.keys) {
     648           0 :           final roomId = MultiKey.fromString(key).parts.first;
     649           0 :           if (rooms.containsKey(roomId)) {
     650           0 :             final raw = await _roomAccountDataBox.get(key);
     651           0 :             final basicRoomEvent = BasicRoomEvent.fromJson(
     652           0 :               convertToJson(raw),
     653             :             );
     654           0 :             rooms[roomId]!.roomAccountData[basicRoomEvent.type] =
     655             :                 basicRoomEvent;
     656             :           } else {
     657           0 :             Logs().w(
     658           0 :                 'Found account data for unknown room $roomId. Delete now...');
     659           0 :             await _roomAccountDataBox.delete(key);
     660             :           }
     661             :         }
     662             : 
     663           2 :         return rooms.values.toList();
     664           3 :       }, _roomsBox.keys.length);
     665             : 
     666           1 :   @override
     667             :   Future<SSSSCache?> getSSSSCache(String type) async {
     668           2 :     final raw = await _ssssCacheBox.get(type);
     669             :     if (raw == null) return null;
     670           2 :     return SSSSCache.fromJson(convertToJson(raw));
     671             :   }
     672             : 
     673           1 :   @override
     674             :   Future<List<QueuedToDeviceEvent>> getToDeviceEventQueue() async =>
     675           5 :       await Future.wait(_toDeviceQueueBox.keys.map((i) async {
     676           2 :         final raw = await _toDeviceQueueBox.get(i);
     677           1 :         raw['id'] = i;
     678           2 :         return QueuedToDeviceEvent.fromJson(convertToJson(raw));
     679           1 :       }).toList());
     680             : 
     681           1 :   @override
     682             :   Future<List<Event>> getUnimportantRoomEventStatesForRoom(
     683             :       List<String> events, Room room) async {
     684           4 :     final keys = _roomStateBox.keys.where((key) {
     685           1 :       final tuple = MultiKey.fromString(key);
     686           4 :       return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
     687             :     });
     688             : 
     689           1 :     final unimportantEvents = <Event>[];
     690           1 :     for (final key in keys) {
     691           0 :       final Map states = await _roomStateBox.get(key);
     692           0 :       unimportantEvents.addAll(
     693           0 :           states.values.map((raw) => Event.fromJson(convertToJson(raw), room)));
     694             :     }
     695           2 :     return unimportantEvents.where((event) => event.stateKey != null).toList();
     696             :   }
     697             : 
     698           1 :   @override
     699             :   Future<User?> getUser(String userId, Room room) async {
     700             :     final state =
     701           5 :         await _roomMembersBox.get(MultiKey(room.id, userId).toString());
     702             :     if (state == null) return null;
     703           0 :     return Event.fromJson(convertToJson(state), room).asUser;
     704             :   }
     705             : 
     706           1 :   @override
     707             :   Future<Map<String, DeviceKeysList>> getUserDeviceKeys(Client client) =>
     708           1 :       runBenchmarked<Map<String, DeviceKeysList>>(
     709           1 :           'Get all user device keys from Hive', () async {
     710           2 :         final deviceKeysOutdated = _userDeviceKeysOutdatedBox.keys;
     711           1 :         if (deviceKeysOutdated.isEmpty) {
     712           1 :           return {};
     713             :         }
     714           0 :         final res = <String, DeviceKeysList>{};
     715           0 :         for (final userId in deviceKeysOutdated) {
     716           0 :           final deviceKeysBoxKeys = _userDeviceKeysBox.keys.where((tuple) {
     717           0 :             final tupleKey = MultiKey.fromString(tuple);
     718           0 :             return tupleKey.parts.first == userId;
     719             :           });
     720             :           final crossSigningKeysBoxKeys =
     721           0 :               _userCrossSigningKeysBox.keys.where((tuple) {
     722           0 :             final tupleKey = MultiKey.fromString(tuple);
     723           0 :             return tupleKey.parts.first == userId;
     724             :           });
     725           0 :           res[userId] = DeviceKeysList.fromDbJson(
     726           0 :               {
     727           0 :                 'client_id': client.id,
     728             :                 'user_id': userId,
     729           0 :                 'outdated': await _userDeviceKeysOutdatedBox.get(userId),
     730             :               },
     731           0 :               await Future.wait(deviceKeysBoxKeys.map((key) async =>
     732           0 :                   convertToJson(await _userDeviceKeysBox.get(key)))),
     733           0 :               await Future.wait(crossSigningKeysBoxKeys.map((key) async =>
     734           0 :                   convertToJson(await _userCrossSigningKeysBox.get(key)))),
     735             :               client);
     736             :         }
     737             :         return res;
     738           3 :       }, _userDeviceKeysBox.keys.length);
     739             : 
     740           1 :   @override
     741             :   Future<List<User>> getUsers(Room room) async {
     742           1 :     final users = <User>[];
     743           2 :     for (final key in _roomMembersBox.keys) {
     744           0 :       final statesKey = MultiKey.fromString(key);
     745           0 :       if (statesKey.parts[0] != room.id) continue;
     746           0 :       final state = await _roomMembersBox.get(key);
     747           0 :       users.add(Event.fromJson(convertToJson(state), room).asUser);
     748             :     }
     749             :     return users;
     750             :   }
     751             : 
     752           1 :   @override
     753             :   Future<void> insertClient(
     754             :       String name,
     755             :       String homeserverUrl,
     756             :       String token,
     757             :       DateTime? tokenExpiresAt,
     758             :       String? refreshToken,
     759             :       String userId,
     760             :       String? deviceId,
     761             :       String? deviceName,
     762             :       String? prevBatch,
     763             :       String? olmAccount) async {
     764           2 :     await _clientBox.put('homeserver_url', homeserverUrl);
     765           2 :     await _clientBox.put('token', token);
     766           2 :     await _clientBox.put(
     767           2 :         'token_expires_at', tokenExpiresAt?.millisecondsSinceEpoch.toString());
     768           2 :     await _clientBox.put('refresh_token', refreshToken);
     769           2 :     await _clientBox.put('user_id', userId);
     770           2 :     await _clientBox.put('device_id', deviceId);
     771           2 :     await _clientBox.put('device_name', deviceName);
     772           2 :     await _clientBox.put('prev_batch', prevBatch);
     773           2 :     await _clientBox.put('olm_account', olmAccount);
     774           2 :     await _clientBox.put('sync_filter_id', null);
     775             :     return;
     776             :   }
     777             : 
     778           1 :   @override
     779             :   Future<int> insertIntoToDeviceQueue(
     780             :       String type, String txnId, String content) async {
     781           3 :     return await _toDeviceQueueBox.add(<String, dynamic>{
     782             :       'type': type,
     783             :       'txn_id': txnId,
     784             :       'content': content,
     785             :     });
     786             :   }
     787             : 
     788           1 :   @override
     789             :   Future<void> markInboundGroupSessionAsUploaded(
     790             :       String roomId, String sessionId) async {
     791           3 :     final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
     792             :     if (raw == null) {
     793           0 :       Logs().w(
     794             :           'Tried to mark inbound group session as uploaded which was not found in the database!');
     795             :       return;
     796             :     }
     797           1 :     raw['uploaded'] = true;
     798           3 :     await _inboundGroupSessionsBox.put(sessionId.toHiveKey, raw);
     799             :     return;
     800             :   }
     801             : 
     802           1 :   @override
     803             :   Future<void> markInboundGroupSessionsAsNeedingUpload() async {
     804           3 :     for (final sessionId in _inboundGroupSessionsBox.keys) {
     805           2 :       final raw = await _inboundGroupSessionsBox.get(sessionId);
     806           1 :       raw['uploaded'] = false;
     807           2 :       await _inboundGroupSessionsBox.put(sessionId, raw);
     808             :     }
     809             :     return;
     810             :   }
     811             : 
     812           1 :   @override
     813             :   Future<void> removeEvent(String eventId, String roomId) async {
     814           4 :     await _eventsBox.delete(MultiKey(roomId, eventId).toString());
     815           3 :     for (final key in _timelineFragmentsBox.keys) {
     816           1 :       final multiKey = MultiKey.fromString(key);
     817           3 :       if (multiKey.parts.first != roomId) continue;
     818           2 :       final List eventIds = await _timelineFragmentsBox.get(key) ?? [];
     819           1 :       final prevLength = eventIds.length;
     820           3 :       eventIds.removeWhere((id) => id == eventId);
     821           2 :       if (eventIds.length < prevLength) {
     822           2 :         await _timelineFragmentsBox.put(key, eventIds);
     823             :       }
     824             :     }
     825             :     return;
     826             :   }
     827             : 
     828           0 :   @override
     829             :   Future<void> removeOutboundGroupSession(String roomId) async {
     830           0 :     await _outboundGroupSessionsBox.delete(roomId.toHiveKey);
     831             :     return;
     832             :   }
     833             : 
     834           1 :   @override
     835             :   Future<void> removeUserCrossSigningKey(
     836             :       String userId, String publicKey) async {
     837           1 :     await _userCrossSigningKeysBox
     838           3 :         .delete(MultiKey(userId, publicKey).toString());
     839             :     return;
     840             :   }
     841             : 
     842           0 :   @override
     843             :   Future<void> removeUserDeviceKey(String userId, String deviceId) async {
     844           0 :     await _userDeviceKeysBox.delete(MultiKey(userId, deviceId).toString());
     845             :     return;
     846             :   }
     847             : 
     848           1 :   @override
     849             :   Future<void> setBlockedUserCrossSigningKey(
     850             :       bool blocked, String userId, String publicKey) async {
     851           1 :     final raw = await _userCrossSigningKeysBox
     852           3 :         .get(MultiKey(userId, publicKey).toString());
     853           1 :     raw['blocked'] = blocked;
     854           2 :     await _userCrossSigningKeysBox.put(
     855           2 :       MultiKey(userId, publicKey).toString(),
     856             :       raw,
     857             :     );
     858             :     return;
     859             :   }
     860             : 
     861           1 :   @override
     862             :   Future<void> setBlockedUserDeviceKey(
     863             :       bool blocked, String userId, String deviceId) async {
     864             :     final raw =
     865           4 :         await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
     866           1 :     raw['blocked'] = blocked;
     867           2 :     await _userDeviceKeysBox.put(
     868           2 :       MultiKey(userId, deviceId).toString(),
     869             :       raw,
     870             :     );
     871             :     return;
     872             :   }
     873             : 
     874           0 :   @override
     875             :   Future<void> setLastActiveUserDeviceKey(
     876             :       int lastActive, String userId, String deviceId) async {
     877             :     final raw =
     878           0 :         await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
     879           0 :     raw['last_active'] = lastActive;
     880           0 :     await _userDeviceKeysBox.put(
     881           0 :       MultiKey(userId, deviceId).toString(),
     882             :       raw,
     883             :     );
     884             :   }
     885             : 
     886           0 :   @override
     887             :   Future<void> setLastSentMessageUserDeviceKey(
     888             :       String lastSentMessage, String userId, String deviceId) async {
     889             :     final raw =
     890           0 :         await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
     891           0 :     raw['last_sent_message'] = lastSentMessage;
     892           0 :     await _userDeviceKeysBox.put(
     893           0 :       MultiKey(userId, deviceId).toString(),
     894             :       raw,
     895             :     );
     896             :   }
     897             : 
     898           1 :   @override
     899             :   Future<void> setRoomPrevBatch(
     900             :       String? prevBatch, String roomId, Client client) async {
     901           3 :     final raw = await _roomsBox.get(roomId.toHiveKey);
     902             :     if (raw == null) return;
     903           2 :     final room = Room.fromJson(convertToJson(raw), client);
     904           1 :     room.prev_batch = prevBatch;
     905           4 :     await _roomsBox.put(roomId.toHiveKey, room.toJson());
     906             :     return;
     907             :   }
     908             : 
     909           1 :   @override
     910             :   Future<void> setVerifiedUserCrossSigningKey(
     911             :       bool verified, String userId, String publicKey) async {
     912           1 :     final raw = (await _userCrossSigningKeysBox
     913           3 :             .get(MultiKey(userId, publicKey).toString()) as Map?) ??
     914           0 :         {};
     915           1 :     raw['verified'] = verified;
     916           2 :     await _userCrossSigningKeysBox.put(
     917           2 :       MultiKey(userId, publicKey).toString(),
     918             :       raw,
     919             :     );
     920             :     return;
     921             :   }
     922             : 
     923           1 :   @override
     924             :   Future<void> setVerifiedUserDeviceKey(
     925             :       bool verified, String userId, String deviceId) async {
     926             :     final raw =
     927           4 :         await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
     928           1 :     raw['verified'] = verified;
     929           2 :     await _userDeviceKeysBox.put(
     930           2 :       MultiKey(userId, deviceId).toString(),
     931             :       raw,
     932             :     );
     933             :     return;
     934             :   }
     935             : 
     936           1 :   @override
     937             :   Future<void> storeAccountData(String type, String content) async {
     938           2 :     await _accountDataBox.put(
     939           3 :         type.toHiveKey, convertToJson(jsonDecode(content)));
     940             :     return;
     941             :   }
     942             : 
     943           1 :   @override
     944             :   Future<void> storeEventUpdate(EventUpdate eventUpdate, Client client) async {
     945             :     // Ephemerals should not be stored
     946           2 :     if (eventUpdate.type == EventUpdateType.ephemeral) return;
     947             : 
     948             :     // In case of this is a redaction event
     949           3 :     if (eventUpdate.content['type'] == EventTypes.Redaction) {
     950           0 :       final tmpRoom = client.getRoomById(eventUpdate.roomID) ??
     951           0 :           Room(id: eventUpdate.roomID, client: client);
     952           0 :       final eventId = eventUpdate.content.tryGet<String>('redacts');
     953             :       final event =
     954           0 :           eventId != null ? await getEventById(eventId, tmpRoom) : null;
     955             :       if (event != null) {
     956           0 :         event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
     957           0 :         await _eventsBox.put(
     958           0 :             MultiKey(eventUpdate.roomID, event.eventId).toString(),
     959           0 :             event.toJson());
     960             : 
     961           0 :         if (tmpRoom.lastEvent?.eventId == event.eventId) {
     962           0 :           await _roomStateBox.put(
     963           0 :             MultiKey(eventUpdate.roomID, event.type).toString(),
     964           0 :             {'': event.toJson()},
     965             :           );
     966             :         }
     967             :       }
     968             :     }
     969             : 
     970             :     // Store a common message event
     971             :     if ({
     972           1 :       EventUpdateType.timeline,
     973           1 :       EventUpdateType.history,
     974           1 :       EventUpdateType.decryptedTimelineQueue
     975           2 :     }.contains(eventUpdate.type)) {
     976           2 :       final eventId = eventUpdate.content['event_id'];
     977             :       // Is this ID already in the store?
     978           1 :       final Map? prevEvent = await _eventsBox
     979           4 :           .get(MultiKey(eventUpdate.roomID, eventId).toString());
     980             :       final prevStatus = prevEvent == null
     981             :           ? null
     982           0 :           : () {
     983           0 :               final json = convertToJson(prevEvent);
     984           0 :               final statusInt = json.tryGet<int>('status') ??
     985             :                   json
     986           0 :                       .tryGetMap<String, dynamic>('unsigned')
     987           0 :                       ?.tryGet<int>(messageSendingStatusKey);
     988           0 :               return statusInt == null ? null : eventStatusFromInt(statusInt);
     989           0 :             }();
     990             : 
     991             :       // calculate the status
     992           1 :       final newStatus = eventStatusFromInt(
     993           2 :         eventUpdate.content.tryGet<int>('status') ??
     994           1 :             eventUpdate.content
     995           1 :                 .tryGetMap<String, dynamic>('unsigned')
     996           0 :                 ?.tryGet<int>(messageSendingStatusKey) ??
     997           1 :             EventStatus.synced.intValue,
     998             :       );
     999             : 
    1000             :       // Is this the response to a sending event which is already synced? Then
    1001             :       // there is nothing to do here.
    1002           1 :       if (!newStatus.isSynced && prevStatus != null && prevStatus.isSynced) {
    1003             :         return;
    1004             :       }
    1005             : 
    1006           1 :       final status = newStatus.isError || prevStatus == null
    1007             :           ? newStatus
    1008           0 :           : latestEventStatus(
    1009             :               prevStatus,
    1010             :               newStatus,
    1011             :             );
    1012             : 
    1013             :       // Add the status and the sort order to the content so it get stored
    1014           3 :       eventUpdate.content['unsigned'] ??= <String, dynamic>{};
    1015           3 :       eventUpdate.content['unsigned'][messageSendingStatusKey] =
    1016           3 :           eventUpdate.content['status'] = status.intValue;
    1017             : 
    1018             :       // In case this event has sent from this account we have a transaction ID
    1019           1 :       final transactionId = eventUpdate.content
    1020           1 :           .tryGetMap<String, dynamic>('unsigned')
    1021           1 :           ?.tryGet<String>('transaction_id');
    1022             : 
    1023           5 :       await _eventsBox.put(MultiKey(eventUpdate.roomID, eventId).toString(),
    1024           1 :           eventUpdate.content);
    1025             : 
    1026             :       // Update timeline fragments
    1027           3 :       final key = MultiKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
    1028           1 :           .toString();
    1029             : 
    1030           3 :       final List eventIds = (await _timelineFragmentsBox.get(key) ?? []);
    1031             : 
    1032           1 :       if (!eventIds.contains(eventId)) {
    1033           2 :         if (eventUpdate.type == EventUpdateType.history) {
    1034           1 :           eventIds.add(eventId);
    1035             :         } else {
    1036           1 :           eventIds.insert(0, eventId);
    1037             :         }
    1038           2 :         await _timelineFragmentsBox.put(key, eventIds);
    1039           0 :       } else if (status.isSynced &&
    1040             :           prevStatus != null &&
    1041           0 :           prevStatus.isSent &&
    1042           0 :           eventUpdate.type != EventUpdateType.history) {
    1043             :         // Status changes from 1 -> 2? Make sure event is correctly sorted.
    1044           0 :         eventIds.remove(eventId);
    1045           0 :         eventIds.insert(0, eventId);
    1046             :       }
    1047             : 
    1048             :       // If event comes from server timeline, remove sending events with this ID
    1049           1 :       if (status.isSent) {
    1050           3 :         final key = MultiKey(eventUpdate.roomID, 'SENDING').toString();
    1051           3 :         final List eventIds = (await _timelineFragmentsBox.get(key) ?? []);
    1052           1 :         final i = eventIds.indexWhere((id) => id == eventId);
    1053           2 :         if (i != -1) {
    1054           0 :           await _timelineFragmentsBox.put(key, eventIds..removeAt(i));
    1055             :         }
    1056             :       }
    1057             : 
    1058             :       // Is there a transaction id? Then delete the event with this id.
    1059           2 :       if (!status.isError && !status.isSending && transactionId != null) {
    1060           0 :         await removeEvent(transactionId, eventUpdate.roomID);
    1061             :       }
    1062             :     }
    1063             : 
    1064           2 :     final stateKey = eventUpdate.content['state_key'];
    1065             :     // Store a common state event
    1066             :     if (stateKey != null &&
    1067             :         // Don't store events as state updates when paginating backwards.
    1068           2 :         (eventUpdate.type == EventUpdateType.timeline ||
    1069           2 :             eventUpdate.type == EventUpdateType.state ||
    1070           2 :             eventUpdate.type == EventUpdateType.inviteState)) {
    1071           3 :       if (eventUpdate.content['type'] == EventTypes.RoomMember) {
    1072           0 :         await _roomMembersBox.put(
    1073           0 :             MultiKey(
    1074           0 :               eventUpdate.roomID,
    1075           0 :               eventUpdate.content['state_key'],
    1076           0 :             ).toString(),
    1077           0 :             eventUpdate.content);
    1078             :       } else {
    1079           1 :         final key = MultiKey(
    1080           1 :           eventUpdate.roomID,
    1081           2 :           eventUpdate.content['type'],
    1082           1 :         ).toString();
    1083           3 :         final Map stateMap = await _roomStateBox.get(key) ?? {};
    1084             : 
    1085           2 :         stateMap[stateKey] = eventUpdate.content;
    1086           2 :         await _roomStateBox.put(key, stateMap);
    1087             :       }
    1088             :     }
    1089             : 
    1090             :     // Store a room account data event
    1091           2 :     if (eventUpdate.type == EventUpdateType.accountData) {
    1092           0 :       await _roomAccountDataBox.put(
    1093           0 :         MultiKey(
    1094           0 :           eventUpdate.roomID,
    1095           0 :           eventUpdate.content['type'],
    1096           0 :         ).toString(),
    1097           0 :         eventUpdate.content,
    1098             :       );
    1099             :     }
    1100             :   }
    1101             : 
    1102           1 :   @override
    1103             :   Future<void> storeFile(Uri mxcUri, Uint8List bytes, int time) async {
    1104             :     return;
    1105             :   }
    1106             : 
    1107           1 :   @override
    1108             :   Future<void> storeInboundGroupSession(
    1109             :       String roomId,
    1110             :       String sessionId,
    1111             :       String pickle,
    1112             :       String content,
    1113             :       String indexes,
    1114             :       String allowedAtIndex,
    1115             :       String senderKey,
    1116             :       String senderClaimedKey) async {
    1117           2 :     await _inboundGroupSessionsBox.put(
    1118           1 :         sessionId.toHiveKey,
    1119           1 :         StoredInboundGroupSession(
    1120             :           roomId: roomId,
    1121             :           sessionId: sessionId,
    1122             :           pickle: pickle,
    1123             :           content: content,
    1124             :           indexes: indexes,
    1125             :           allowedAtIndex: allowedAtIndex,
    1126             :           senderKey: senderKey,
    1127             :           senderClaimedKeys: senderClaimedKey,
    1128             :           uploaded: false,
    1129           1 :         ).toJson());
    1130             :     return;
    1131             :   }
    1132             : 
    1133           1 :   @override
    1134             :   Future<void> storeOutboundGroupSession(
    1135             :       String roomId, String pickle, String deviceIds, int creationTime) async {
    1136           4 :     await _outboundGroupSessionsBox.put(roomId.toHiveKey, <String, dynamic>{
    1137             :       'room_id': roomId,
    1138             :       'pickle': pickle,
    1139             :       'device_ids': deviceIds,
    1140             :       'creation_time': creationTime,
    1141             :     });
    1142             :     return;
    1143             :   }
    1144             : 
    1145           0 :   @override
    1146             :   Future<void> storePrevBatch(
    1147             :     String prevBatch,
    1148             :   ) async {
    1149           0 :     if (_clientBox.keys.isEmpty) return;
    1150           0 :     await _clientBox.put('prev_batch', prevBatch);
    1151             :     return;
    1152             :   }
    1153             : 
    1154           1 :   @override
    1155             :   Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
    1156             :       Event? lastEvent, Client client) async {
    1157             :     // Leave room if membership is leave
    1158           1 :     if (roomUpdate is LeftRoomUpdate) {
    1159           0 :       await forgetRoom(roomId);
    1160             :       return;
    1161             :     }
    1162           1 :     final membership = roomUpdate is LeftRoomUpdate
    1163             :         ? Membership.leave
    1164           1 :         : roomUpdate is InvitedRoomUpdate
    1165             :             ? Membership.invite
    1166             :             : Membership.join;
    1167             :     // Make sure room exists
    1168           3 :     if (!_roomsBox.containsKey(roomId.toHiveKey)) {
    1169           2 :       await _roomsBox.put(
    1170           1 :           roomId.toHiveKey,
    1171           1 :           roomUpdate is JoinedRoomUpdate
    1172           1 :               ? Room(
    1173             :                   client: client,
    1174             :                   id: roomId,
    1175             :                   membership: membership,
    1176             :                   highlightCount:
    1177           1 :                       roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
    1178             :                           0,
    1179             :                   notificationCount: roomUpdate
    1180           1 :                           .unreadNotifications?.notificationCount
    1181           0 :                           ?.toInt() ??
    1182             :                       0,
    1183           1 :                   prev_batch: roomUpdate.timeline?.prevBatch,
    1184           1 :                   summary: roomUpdate.summary,
    1185             :                   lastEvent: lastEvent,
    1186           1 :                 ).toJson()
    1187           0 :               : Room(
    1188             :                   client: client,
    1189             :                   id: roomId,
    1190             :                   membership: membership,
    1191             :                   lastEvent: lastEvent,
    1192           0 :                 ).toJson());
    1193           0 :     } else if (roomUpdate is JoinedRoomUpdate) {
    1194           0 :       final currentRawRoom = await _roomsBox.get(roomId.toHiveKey);
    1195           0 :       final currentRoom = Room.fromJson(convertToJson(currentRawRoom), client);
    1196           0 :       await _roomsBox.put(
    1197           0 :           roomId.toHiveKey,
    1198           0 :           Room(
    1199             :             client: client,
    1200             :             id: roomId,
    1201             :             membership: membership,
    1202             :             highlightCount:
    1203           0 :                 roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
    1204           0 :                     currentRoom.highlightCount,
    1205             :             notificationCount:
    1206           0 :                 roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
    1207           0 :                     currentRoom.notificationCount,
    1208             :             prev_batch:
    1209           0 :                 roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
    1210           0 :             summary: RoomSummary.fromJson(currentRoom.summary.toJson()
    1211           0 :               ..addAll(roomUpdate.summary?.toJson() ?? {})),
    1212           0 :           ).toJson());
    1213             :     }
    1214             :   }
    1215             : 
    1216           0 :   @override
    1217             :   Future<void> deleteTimelineForRoom(String roomId) =>
    1218           0 :       _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
    1219             : 
    1220           1 :   @override
    1221             :   Future<void> storeSSSSCache(
    1222             :       String type, String keyId, String ciphertext, String content) async {
    1223           2 :     await _ssssCacheBox.put(
    1224             :         type,
    1225           1 :         SSSSCache(
    1226             :           type: type,
    1227             :           keyId: keyId,
    1228             :           ciphertext: ciphertext,
    1229             :           content: content,
    1230           1 :         ).toJson());
    1231             :   }
    1232             : 
    1233           1 :   @override
    1234             :   Future<void> storeSyncFilterId(
    1235             :     String syncFilterId,
    1236             :   ) async {
    1237           2 :     await _clientBox.put('sync_filter_id', syncFilterId);
    1238             :   }
    1239             : 
    1240           1 :   @override
    1241             :   Future<void> storeUserCrossSigningKey(String userId, String publicKey,
    1242             :       String content, bool verified, bool blocked) async {
    1243           2 :     await _userCrossSigningKeysBox.put(
    1244           2 :       MultiKey(userId, publicKey).toString(),
    1245           1 :       {
    1246             :         'user_id': userId,
    1247             :         'public_key': publicKey,
    1248             :         'content': content,
    1249             :         'verified': verified,
    1250             :         'blocked': blocked,
    1251             :       },
    1252             :     );
    1253             :   }
    1254             : 
    1255           1 :   @override
    1256             :   Future<void> storeUserDeviceKey(String userId, String deviceId,
    1257             :       String content, bool verified, bool blocked, int lastActive) async {
    1258           5 :     await _userDeviceKeysBox.put(MultiKey(userId, deviceId).toString(), {
    1259             :       'user_id': userId,
    1260             :       'device_id': deviceId,
    1261             :       'content': content,
    1262             :       'verified': verified,
    1263             :       'blocked': blocked,
    1264             :       'last_active': lastActive,
    1265             :       'last_sent_message': '',
    1266             :     });
    1267             :     return;
    1268             :   }
    1269             : 
    1270           1 :   @override
    1271             :   Future<void> storeUserDeviceKeysInfo(String userId, bool outdated) async {
    1272           3 :     await _userDeviceKeysOutdatedBox.put(userId.toHiveKey, outdated);
    1273             :     return;
    1274             :   }
    1275             : 
    1276           1 :   @override
    1277             :   Future<void> transaction(Future<void> Function() action) =>
    1278           1 :       zoneTransaction(action);
    1279             : 
    1280           1 :   @override
    1281             :   Future<void> updateClient(
    1282             :     String homeserverUrl,
    1283             :     String token,
    1284             :     DateTime? tokenExpiresAt,
    1285             :     String? refreshToken,
    1286             :     String userId,
    1287             :     String? deviceId,
    1288             :     String? deviceName,
    1289             :     String? prevBatch,
    1290             :     String? olmAccount,
    1291             :   ) async {
    1292           2 :     await _clientBox.put('homeserver_url', homeserverUrl);
    1293           2 :     await _clientBox.put('token', token);
    1294           2 :     await _clientBox.put(
    1295           2 :         'token_expires_at', tokenExpiresAt?.millisecondsSinceEpoch.toString());
    1296           2 :     await _clientBox.put('refresh_token', refreshToken);
    1297           2 :     await _clientBox.put('user_id', userId);
    1298           2 :     await _clientBox.put('device_id', deviceId);
    1299           2 :     await _clientBox.put('device_name', deviceName);
    1300           2 :     await _clientBox.put('prev_batch', prevBatch);
    1301           2 :     await _clientBox.put('olm_account', olmAccount);
    1302             :     return;
    1303             :   }
    1304             : 
    1305           1 :   @override
    1306             :   Future<void> updateClientKeys(
    1307             :     String olmAccount,
    1308             :   ) async {
    1309           2 :     await _clientBox.put('olm_account', olmAccount);
    1310             :     return;
    1311             :   }
    1312             : 
    1313           1 :   @override
    1314             :   Future<void> updateInboundGroupSessionAllowedAtIndex(
    1315             :       String allowedAtIndex, String roomId, String sessionId) async {
    1316           3 :     final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
    1317             :     if (raw == null) {
    1318           0 :       Logs().w(
    1319             :           'Tried to update inbound group session as uploaded which wasnt found in the database!');
    1320             :       return;
    1321             :     }
    1322           1 :     raw['allowed_at_index'] = allowedAtIndex;
    1323           3 :     await _inboundGroupSessionsBox.put(sessionId.toHiveKey, raw);
    1324             :     return;
    1325             :   }
    1326             : 
    1327           1 :   @override
    1328             :   Future<void> updateInboundGroupSessionIndexes(
    1329             :       String indexes, String roomId, String sessionId) async {
    1330           3 :     final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
    1331             :     if (raw == null) {
    1332           0 :       Logs().w(
    1333             :           'Tried to update inbound group session indexes of a session which was not found in the database!');
    1334             :       return;
    1335             :     }
    1336           1 :     raw['indexes'] = indexes;
    1337           3 :     await _inboundGroupSessionsBox.put(sessionId.toHiveKey, raw);
    1338             :     return;
    1339             :   }
    1340             : 
    1341           1 :   @override
    1342             :   Future<List<StoredInboundGroupSession>> getAllInboundGroupSessions() async {
    1343           3 :     final rawSessions = await Future.wait(_inboundGroupSessionsBox.keys
    1344           1 :         .map((key) => _inboundGroupSessionsBox.get(key)));
    1345             :     return rawSessions
    1346           1 :         .map((raw) => StoredInboundGroupSession.fromJson(convertToJson(raw)))
    1347           1 :         .toList();
    1348             :   }
    1349             : 
    1350           0 :   @override
    1351             :   Future<void> addSeenDeviceId(
    1352             :     String userId,
    1353             :     String deviceId,
    1354             :     String publicKeys,
    1355             :   ) =>
    1356           0 :       _seenDeviceIdsBox.put(MultiKey(userId, deviceId).toString(), publicKeys);
    1357             : 
    1358           0 :   @override
    1359             :   Future<void> addSeenPublicKey(
    1360             :     String publicKey,
    1361             :     String deviceId,
    1362             :   ) =>
    1363           0 :       _seenDeviceKeysBox.put(publicKey.toHiveKey, deviceId);
    1364             : 
    1365           0 :   @override
    1366             :   Future<String?> deviceIdSeen(userId, deviceId) async {
    1367             :     final raw =
    1368           0 :         await _seenDeviceIdsBox.get(MultiKey(userId, deviceId).toString());
    1369             :     if (raw == null) return null;
    1370             :     return raw as String;
    1371             :   }
    1372             : 
    1373           0 :   @override
    1374             :   Future<String?> publicKeySeen(String publicKey) async {
    1375           0 :     final raw = await _seenDeviceKeysBox.get(publicKey.toHiveKey);
    1376             :     if (raw == null) return null;
    1377             :     return raw as String;
    1378             :   }
    1379             : 
    1380           1 :   @override
    1381             :   Future<void> storePresence(String userId, CachedPresence presence) =>
    1382           3 :       _presencesBox.put(userId, presence.toJson());
    1383             : 
    1384           1 :   @override
    1385             :   Future<CachedPresence?> getPresence(String userId) async {
    1386           2 :     final rawPresence = await _presencesBox.get(userId);
    1387             :     if (rawPresence == null) return null;
    1388             : 
    1389           2 :     return CachedPresence.fromJson(copyMap(rawPresence));
    1390             :   }
    1391             : 
    1392           0 :   @override
    1393             :   Future<String> exportDump() {
    1394             :     // see no need to implement this in a deprecated part
    1395           0 :     throw UnimplementedError();
    1396             :   }
    1397             : 
    1398           0 :   @override
    1399             :   Future<bool> importDump(String export) {
    1400             :     // see no need to implement this in a deprecated part
    1401           0 :     throw UnimplementedError();
    1402             :   }
    1403             : 
    1404           0 :   @override
    1405             :   Future<void> storeWellKnown(DiscoveryInformation? discoveryInformation) {
    1406             :     if (discoveryInformation == null) {
    1407           0 :       return _clientBox.delete('discovery_information');
    1408             :     }
    1409           0 :     return _clientBox.put(
    1410             :       'discovery_information',
    1411           0 :       jsonEncode(discoveryInformation.toJson()),
    1412             :     );
    1413             :   }
    1414             : 
    1415           0 :   @override
    1416             :   Future<DiscoveryInformation?> getWellKnown() async {
    1417             :     final rawDiscoveryInformation =
    1418           0 :         await _clientBox.get('discovery_information');
    1419             :     if (rawDiscoveryInformation == null) return null;
    1420           0 :     return DiscoveryInformation.fromJson(jsonDecode(rawDiscoveryInformation));
    1421             :   }
    1422             : 
    1423           0 :   @override
    1424           0 :   Future<void> delete() => Hive.deleteFromDisk();
    1425             : 
    1426           0 :   @override
    1427             :   Future<void> markUserProfileAsOutdated(userId) async {
    1428             :     return;
    1429             :   }
    1430             : 
    1431           1 :   @override
    1432             :   Future<CachedProfileInformation?> getUserProfile(String userId) async {
    1433             :     return null;
    1434             :   }
    1435             : 
    1436           1 :   @override
    1437             :   Future<void> storeUserProfile(
    1438             :       String userId, CachedProfileInformation profile) async {
    1439             :     return;
    1440             :   }
    1441             : }
    1442             : 
    1443           1 : dynamic _castValue(dynamic value) {
    1444           1 :   if (value is Map) {
    1445           1 :     return convertToJson(value);
    1446             :   }
    1447           1 :   if (value is List) {
    1448           2 :     return value.map(_castValue).toList();
    1449             :   }
    1450             :   return value;
    1451             : }
    1452             : 
    1453             : /// Hive always gives back an `_InternalLinkedHasMap<dynamic, dynamic>`. This
    1454             : /// creates a deep copy of the json and makes sure that the format is always
    1455             : /// `Map<String, dynamic>`.
    1456           1 : Map<String, dynamic> convertToJson(Map map) {
    1457           1 :   final copy = Map<String, dynamic>.from(map);
    1458           2 :   for (final entry in copy.entries) {
    1459           4 :     copy[entry.key] = _castValue(entry.value);
    1460             :   }
    1461             :   return copy;
    1462             : }
    1463             : 
    1464             : class MultiKey {
    1465             :   final List<String> parts;
    1466             : 
    1467           1 :   MultiKey(String key1, [String? key2, String? key3])
    1468           1 :       : parts = [
    1469             :           key1,
    1470           1 :           if (key2 != null) key2,
    1471           0 :           if (key3 != null) key3,
    1472             :         ];
    1473             : 
    1474           0 :   const MultiKey.byParts(this.parts);
    1475             : 
    1476           1 :   MultiKey.fromString(String multiKeyString)
    1477           5 :       : parts = multiKeyString.split('|').map((s) => s.fromHiveKey).toList();
    1478             : 
    1479           1 :   @override
    1480           5 :   String toString() => parts.map((s) => s.toHiveKey).join('|');
    1481             : 
    1482           0 :   @override
    1483           0 :   bool operator ==(other) => parts.toString() == other.toString();
    1484             : 
    1485           0 :   @override
    1486           0 :   int get hashCode => Object.hashAll(parts);
    1487             : }
    1488             : 
    1489             : extension HiveKeyExtension on String {
    1490           2 :   String get toHiveKey => isValidMatrixId
    1491           5 :       ? '$sigil${Uri.encodeComponent(localpart!)}:${Uri.encodeComponent(domain!)}'
    1492           2 :       : Uri.encodeComponent(this);
    1493             : }
    1494             : 
    1495             : extension FromHiveKeyExtension on String {
    1496           2 :   String get fromHiveKey => Uri.decodeComponent(this);
    1497             : }

Generated by: LCOV version 1.14