From 135face535d4845c2995bdef3b182e02adea4cf8 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 30 Dec 2024 12:58:10 +0100 Subject: [PATCH] refactor: (BREAKING) Remove deprecated Hive Database --- lib/matrix.dart | 1 - lib/src/database/hive_database.dart | 1644 --------------------------- test/database_api_test.dart | 5 +- test/fake_database.dart | 15 - 4 files changed, 1 insertion(+), 1664 deletions(-) delete mode 100644 lib/src/database/hive_database.dart diff --git a/lib/matrix.dart b/lib/matrix.dart index f7e30e06..c3219e84 100644 --- a/lib/matrix.dart +++ b/lib/matrix.dart @@ -25,7 +25,6 @@ export 'fake_matrix_api.dart' show FakeMatrixApi; export 'src/client.dart'; export 'src/database/database_api.dart'; -export 'src/database/hive_database.dart'; export 'src/database/matrix_sdk_database.dart'; export 'src/database/hive_collections_database.dart'; export 'src/database/sqflite_encryption_helper.dart'; diff --git a/lib/src/database/hive_database.dart b/lib/src/database/hive_database.dart deleted file mode 100644 index 78f46d9c..00000000 --- a/lib/src/database/hive_database.dart +++ /dev/null @@ -1,1644 +0,0 @@ -/* - * Famedly Matrix SDK - * Copyright (C) 2019, 2020, 2021 Famedly GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:hive/hive.dart'; - -import 'package:matrix/encryption/utils/olm_session.dart'; -import 'package:matrix/encryption/utils/outbound_group_session.dart'; -import 'package:matrix/encryption/utils/ssss_cache.dart'; -import 'package:matrix/encryption/utils/stored_inbound_group_session.dart'; -import 'package:matrix/matrix.dart'; -import 'package:matrix/src/database/zone_transaction_mixin.dart'; -import 'package:matrix/src/utils/copy_map.dart'; -import 'package:matrix/src/utils/queued_to_device_event.dart'; -import 'package:matrix/src/utils/run_benchmarked.dart'; - -/// This is a basic database for the Matrix SDK using the hive store. You need -/// to make sure that you perform `Hive.init()` or `Hive.flutterInit()` before -/// you use this. -/// -/// This database does not support file caching! -@Deprecated( - 'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!', -) -class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { - static const int version = 6; - final String name; - late Box _clientBox; - late Box _accountDataBox; - late Box _roomsBox; - late Box _toDeviceQueueBox; - - /// Key is a tuple as MultiKey(roomId, type) where stateKey can be - /// an empty string. - late LazyBox _roomStateBox; - - /// Key is a tuple as MultiKey(roomId, userId) - late LazyBox _roomMembersBox; - - /// Key is a tuple as MultiKey(roomId, type) - late LazyBox _roomAccountDataBox; - late LazyBox _inboundGroupSessionsBox; - late LazyBox _outboundGroupSessionsBox; - late LazyBox _olmSessionsBox; - - /// Key is a tuple as MultiKey(userId, deviceId) - late LazyBox _userDeviceKeysBox; - - /// Key is the user ID as a String - late LazyBox _userDeviceKeysOutdatedBox; - - /// Key is a tuple as MultiKey(userId, publicKey) - late LazyBox _userCrossSigningKeysBox; - late LazyBox _ssssCacheBox; - late LazyBox _presencesBox; - - /// Key is a tuple as Multikey(roomId, fragmentId) while the default - /// fragmentId is an empty String - late LazyBox _timelineFragmentsBox; - - /// Key is a tuple as MultiKey(roomId, eventId) - late LazyBox _eventsBox; - - /// Key is a tuple as MultiKey(userId, deviceId) - late LazyBox _seenDeviceIdsBox; - - late LazyBox _seenDeviceKeysBox; - - String get _clientBoxName => '$name.box.client'; - - String get _accountDataBoxName => '$name.box.account_data'; - - String get _roomsBoxName => '$name.box.rooms'; - - String get _toDeviceQueueBoxName => '$name.box.to_device_queue'; - - String get _roomStateBoxName => '$name.box.room_states'; - - String get _roomMembersBoxName => '$name.box.room_members'; - - String get _roomAccountDataBoxName => '$name.box.room_account_data'; - - String get _inboundGroupSessionsBoxName => '$name.box.inbound_group_session'; - - String get _outboundGroupSessionsBoxName => - '$name.box.outbound_group_session'; - - String get _olmSessionsBoxName => '$name.box.olm_session'; - - String get _userDeviceKeysBoxName => '$name.box.user_device_keys'; - - String get _userDeviceKeysOutdatedBoxName => - '$name.box.user_device_keys_outdated'; - - String get _userCrossSigningKeysBoxName => '$name.box.cross_signing_keys'; - - String get _ssssCacheBoxName => '$name.box.ssss_cache'; - - String get _presencesBoxName => '$name.box.presences'; - - String get _timelineFragmentsBoxName => '$name.box.timeline_fragments'; - - String get _eventsBoxName => '$name.box.events'; - - String get _seenDeviceIdsBoxName => '$name.box.seen_device_ids'; - - String get _seenDeviceKeysBoxName => '$name.box.seen_device_keys'; - - final HiveCipher? encryptionCipher; - - FamedlySdkHiveDatabase(this.name, {this.encryptionCipher}); - - @override - int get maxFileSize => 0; - - Future _actionOnAllBoxes(Future Function(BoxBase box) action) => - Future.wait([ - action(_clientBox), - action(_accountDataBox), - action(_roomsBox), - action(_roomStateBox), - action(_roomMembersBox), - action(_toDeviceQueueBox), - action(_roomAccountDataBox), - action(_inboundGroupSessionsBox), - action(_outboundGroupSessionsBox), - action(_olmSessionsBox), - action(_userDeviceKeysBox), - action(_userDeviceKeysOutdatedBox), - action(_userCrossSigningKeysBox), - action(_ssssCacheBox), - action(_presencesBox), - action(_timelineFragmentsBox), - action(_eventsBox), - action(_seenDeviceIdsBox), - action(_seenDeviceKeysBox), - ]); - - Future open() async { - _clientBox = await Hive.openBox( - _clientBoxName, - encryptionCipher: encryptionCipher, - ); - _accountDataBox = await Hive.openBox( - _accountDataBoxName, - encryptionCipher: encryptionCipher, - ); - _roomsBox = await Hive.openBox( - _roomsBoxName, - encryptionCipher: encryptionCipher, - ); - _roomStateBox = await Hive.openLazyBox( - _roomStateBoxName, - encryptionCipher: encryptionCipher, - ); - _roomMembersBox = await Hive.openLazyBox( - _roomMembersBoxName, - encryptionCipher: encryptionCipher, - ); - _toDeviceQueueBox = await Hive.openBox( - _toDeviceQueueBoxName, - encryptionCipher: encryptionCipher, - ); - _roomAccountDataBox = await Hive.openLazyBox( - _roomAccountDataBoxName, - encryptionCipher: encryptionCipher, - ); - _inboundGroupSessionsBox = await Hive.openLazyBox( - _inboundGroupSessionsBoxName, - encryptionCipher: encryptionCipher, - ); - _outboundGroupSessionsBox = await Hive.openLazyBox( - _outboundGroupSessionsBoxName, - encryptionCipher: encryptionCipher, - ); - _olmSessionsBox = await Hive.openLazyBox( - _olmSessionsBoxName, - encryptionCipher: encryptionCipher, - ); - _userDeviceKeysBox = await Hive.openLazyBox( - _userDeviceKeysBoxName, - encryptionCipher: encryptionCipher, - ); - _userDeviceKeysOutdatedBox = await Hive.openLazyBox( - _userDeviceKeysOutdatedBoxName, - encryptionCipher: encryptionCipher, - ); - _userCrossSigningKeysBox = await Hive.openLazyBox( - _userCrossSigningKeysBoxName, - encryptionCipher: encryptionCipher, - ); - _ssssCacheBox = await Hive.openLazyBox( - _ssssCacheBoxName, - encryptionCipher: encryptionCipher, - ); - _presencesBox = await Hive.openLazyBox( - _presencesBoxName, - encryptionCipher: encryptionCipher, - ); - _timelineFragmentsBox = await Hive.openLazyBox( - _timelineFragmentsBoxName, - encryptionCipher: encryptionCipher, - ); - _eventsBox = await Hive.openLazyBox( - _eventsBoxName, - encryptionCipher: encryptionCipher, - ); - _seenDeviceIdsBox = await Hive.openLazyBox( - _seenDeviceIdsBoxName, - encryptionCipher: encryptionCipher, - ); - _seenDeviceKeysBox = await Hive.openLazyBox( - _seenDeviceKeysBoxName, - encryptionCipher: encryptionCipher, - ); - - // Check version and check if we need a migration - final currentVersion = (await _clientBox.get('version') as int?); - if (currentVersion == null) { - await _clientBox.put('version', version); - } else if (currentVersion != version) { - await _migrateFromVersion(currentVersion); - } - - return; - } - - Future _migrateFromVersion(int currentVersion) async { - Logs().i('Migrate Hive database from version $currentVersion to $version'); - if (version == 5) { - for (final key in _userDeviceKeysBox.keys) { - try { - final raw = await _userDeviceKeysBox.get(key) as Map; - if (!raw.containsKey('keys')) continue; - final deviceKeys = DeviceKeys.fromJson( - convertToJson(raw), - Client(''), - ); - await addSeenDeviceId( - deviceKeys.userId, - deviceKeys.deviceId!, - deviceKeys.curve25519Key! + deviceKeys.ed25519Key!, - ); - await addSeenPublicKey(deviceKeys.ed25519Key!, deviceKeys.deviceId!); - await addSeenPublicKey( - deviceKeys.curve25519Key!, - deviceKeys.deviceId!, - ); - } catch (e) { - Logs().w('Can not migrate device $key', e); - } - } - } - await clearCache(); - await _clientBox.put('version', version); - } - - @override - Future clear() async { - Logs().i('Clear and close hive database...'); - await _actionOnAllBoxes((box) async { - try { - await box.deleteAll(box.keys); - await box.close(); - } catch (e) { - Logs().v('Unable to clear box ${box.name}', e); - await box.deleteFromDisk(); - } - }); - return; - } - - @override - Future clearCache() async { - await _roomsBox.deleteAll(_roomsBox.keys); - await _accountDataBox.deleteAll(_accountDataBox.keys); - await _roomAccountDataBox.deleteAll(_roomAccountDataBox.keys); - await _roomStateBox.deleteAll(_roomStateBox.keys); - await _roomMembersBox.deleteAll(_roomMembersBox.keys); - await _eventsBox.deleteAll(_eventsBox.keys); - await _timelineFragmentsBox.deleteAll(_timelineFragmentsBox.keys); - await _outboundGroupSessionsBox.deleteAll(_outboundGroupSessionsBox.keys); - await _presencesBox.deleteAll(_presencesBox.keys); - await _clientBox.delete('prev_batch'); - } - - @override - Future clearSSSSCache() async { - await _ssssCacheBox.deleteAll(_ssssCacheBox.keys); - } - - @override - Future close() => _actionOnAllBoxes((box) => box.close()); - - @override - Future deleteFromToDeviceQueue(int id) async { - await _toDeviceQueueBox.delete(id); - return; - } - - @override - Future deleteOldFiles(int savedAt) async { - return; - } - - @override - Future forgetRoom(String roomId) async { - await _timelineFragmentsBox.delete(MultiKey(roomId, '').toString()); - for (final key in _eventsBox.keys) { - final multiKey = MultiKey.fromString(key); - if (multiKey.parts.first != roomId) continue; - await _eventsBox.delete(key); - } - for (final key in _roomStateBox.keys) { - final multiKey = MultiKey.fromString(key); - if (multiKey.parts.first != roomId) continue; - await _roomStateBox.delete(key); - } - for (final key in _roomMembersBox.keys) { - final multiKey = MultiKey.fromString(key); - if (multiKey.parts.first != roomId) continue; - await _roomMembersBox.delete(key); - } - for (final key in _roomAccountDataBox.keys) { - final multiKey = MultiKey.fromString(key); - if (multiKey.parts.first != roomId) continue; - await _roomAccountDataBox.delete(key); - } - await _roomsBox.delete(roomId.toHiveKey); - } - - @override - Future> getAccountData() => - runBenchmarked>( - 'Get all account data from Hive', - () async { - final accountData = {}; - for (final key in _accountDataBox.keys) { - final raw = await _accountDataBox.get(key); - accountData[key.toString().fromHiveKey] = BasicEvent( - type: key.toString().fromHiveKey, - content: convertToJson(raw), - ); - } - return accountData; - }, - _accountDataBox.keys.length, - ); - - @override - Future?> getClient(String name) => - runBenchmarked('Get Client from Hive', () async { - final map = {}; - for (final key in _clientBox.keys) { - if (key == 'version') continue; - map[key] = await _clientBox.get(key); - } - if (map.isEmpty) return null; - return map; - }); - - @override - Future getEventById(String eventId, Room room) async { - final raw = await _eventsBox.get(MultiKey(room.id, eventId).toString()); - if (raw == null) return null; - return Event.fromJson(convertToJson(raw), room); - } - - /// Loads a whole list of events at once from the store for a specific room - Future> _getEventsByIds(List eventIds, Room room) async { - final events = await Future.wait( - eventIds.map((String eventId) async { - final entry = - await _eventsBox.get(MultiKey(room.id, eventId).toString()); - return entry is Map ? Event.fromJson(convertToJson(entry), room) : null; - }), - ); - - return events.whereType().toList(); - } - - @override - Future> getEventList( - Room room, { - int start = 0, - bool onlySending = false, - int? limit, - }) => - runBenchmarked>('Get event list', () async { - // Get the synced event IDs from the store - final timelineKey = MultiKey(room.id, '').toString(); - final timelineEventIds = List.from( - (await _timelineFragmentsBox.get(timelineKey)) ?? [], - ); - // Get the local stored SENDING events from the store - late final List sendingEventIds; - if (start != 0) { - sendingEventIds = []; - } else { - final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString(); - sendingEventIds = List.from( - (await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [], - ); - } - - // Combine those two lists while respecting the start and limit parameters. - final end = min( - timelineEventIds.length, - start + (limit ?? timelineEventIds.length), - ); - final eventIds = List.from( - [ - ...sendingEventIds, - ...(start < timelineEventIds.length && !onlySending - ? timelineEventIds.getRange(start, end).toList() - : []), - ], - ); - - return await _getEventsByIds(eventIds, room); - }); - - @override - Future> getEventIdList( - Room room, { - int start = 0, - bool includeSending = false, - int? limit, - }) => - runBenchmarked>('Get event id list', () async { - // Get the synced event IDs from the store - final timelineKey = MultiKey(room.id, '').toString(); - - final timelineEventIds = List.from( - (await _timelineFragmentsBox.get(timelineKey)) ?? [], - ); - - // Get the local stored SENDING events from the store - late final List sendingEventIds; - if (!includeSending) { - sendingEventIds = []; - } else { - final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString(); - sendingEventIds = List.from( - (await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [], - ); - } - - // Combine those two lists while respecting the start and limit parameters. - final eventIds = sendingEventIds + timelineEventIds; - if (limit != null && eventIds.length > limit) { - eventIds.removeRange(limit, eventIds.length); - } - - return eventIds; - }); - - @override - Future getFile(Uri mxcUri) async { - return null; - } - - @override - Future deleteFile(Uri mxcUri) async { - return false; - } - - @override - Future getInboundGroupSession( - String roomId, - String sessionId, - ) async { - final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey); - if (raw == null) return null; - return StoredInboundGroupSession.fromJson(convertToJson(raw)); - } - - @override - Future> - getInboundGroupSessionsToUpload() async { - final sessions = (await Future.wait( - _inboundGroupSessionsBox.keys.map( - (sessionId) async => await _inboundGroupSessionsBox.get(sessionId), - ), - )) - .where((rawSession) => rawSession['uploaded'] == false) - .take(500) - .map( - (json) => StoredInboundGroupSession.fromJson( - convertToJson(json), - ), - ) - .toList(); - return sessions; - } - - @override - Future> getLastSentMessageUserDeviceKey( - String userId, - String deviceId, - ) async { - final raw = - await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString()); - if (raw == null) return []; - return [raw['last_sent_message']]; - } - - @override - Future storeOlmSession( - String identityKey, - String sessionId, - String pickle, - int lastReceived, - ) async { - final rawSessions = - (await _olmSessionsBox.get(identityKey.toHiveKey) as Map?) ?? {}; - rawSessions[sessionId] = { - 'identity_key': identityKey, - 'pickle': pickle, - 'session_id': sessionId, - 'last_received': lastReceived, - }; - await _olmSessionsBox.put(identityKey.toHiveKey, rawSessions); - return; - } - - @override - Future> getOlmSessions( - String identityKey, - String userId, - ) async { - final rawSessions = - await _olmSessionsBox.get(identityKey.toHiveKey) as Map? ?? {}; - - return rawSessions.values - .map((json) => OlmSession.fromJson(convertToJson(json), userId)) - .toList(); - } - - @override - Future> getAllOlmSessions() async { - final backup = Map.fromEntries( - await Future.wait( - _olmSessionsBox.keys.map( - (key) async => MapEntry( - key, - await _olmSessionsBox.get(key), - ), - ), - ), - ); - return backup.cast(); - } - - @override - Future> getOlmSessionsForDevices( - List identityKeys, - String userId, - ) async { - final sessions = await Future.wait( - identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)), - ); - return [for (final sublist in sessions) ...sublist]; - } - - @override - Future getOutboundGroupSession( - String roomId, - String userId, - ) async { - final raw = await _outboundGroupSessionsBox.get(roomId.toHiveKey); - if (raw == null) return null; - return OutboundGroupSession.fromJson(convertToJson(raw), userId); - } - - @override - Future getSingleRoom( - Client client, - String roomId, { - bool loadImportantStates = true, - }) async { - // Get raw room from database: - final roomData = await _roomsBox.get(roomId); - if (roomData == null) return null; - final room = Room.fromJson(convertToJson(roomData), client); - - // Get important states: - if (loadImportantStates) { - final dbKeys = client.importantStateEvents - .map((state) => TupleKey(roomId, state).toString()) - .toList(); - final rawStates = await Future.wait( - dbKeys.map((key) => _roomStateBox.get(key)), - ); - for (final rawState in rawStates) { - if (rawState == null || rawState[''] == null) continue; - room.setState(Event.fromJson(convertToJson(rawState['']), room)); - } - } - - return room; - } - - @override - Future> getRoomList(Client client) => runBenchmarked>( - 'Get room list from hive', - () async { - final rooms = {}; - final userID = client.userID; - final importantRoomStates = client.importantStateEvents; - for (final key in _roomsBox.keys) { - // Get the room - final raw = await _roomsBox.get(key); - final room = Room.fromJson(convertToJson(raw), client); - - // let's see if we need any m.room.member events - // We always need the member event for ourself - final membersToPostload = {if (userID != null) userID}; - // If the room is a direct chat, those IDs should be there too - if (room.isDirectChat) { - membersToPostload.add(room.directChatMatrixID!); - } - // the lastEvent message preview might have an author we need to fetch, if it is a group chat - final lastEvent = room.getState(EventTypes.Message); - if (lastEvent != null && !room.isDirectChat) { - membersToPostload.add(lastEvent.senderId); - } - // if the room has no name and no canonical alias, its name is calculated - // based on the heroes of the room - if (room.getState(EventTypes.RoomName) == null && - room.getState(EventTypes.RoomCanonicalAlias) == null) { - // we don't have a name and no canonical alias, so we'll need to - // post-load the heroes - membersToPostload.addAll(room.summary.mHeroes ?? []); - } - // Load members - for (final userId in membersToPostload) { - final state = await _roomMembersBox - .get(MultiKey(room.id, userId).toString()); - if (state == null) { - Logs().w('Unable to post load member $userId'); - continue; - } - room.setState( - room.membership == Membership.invite - ? StrippedStateEvent.fromJson(copyMap(raw)) - : Event.fromJson(convertToJson(state), room), - ); - } - - // Get the "important" room states. All other states will be loaded once - // `getUnimportantRoomStates()` is called. - for (final type in importantRoomStates) { - final states = await _roomStateBox - .get(MultiKey(room.id, type).toString()) as Map?; - if (states == null) continue; - final stateEvents = states.values - .map( - (raw) => room.membership == Membership.invite - ? StrippedStateEvent.fromJson(copyMap(raw)) - : Event.fromJson(convertToJson(raw), room), - ) - .toList(); - for (final state in stateEvents) { - room.setState(state); - } - } - - // Add to the list and continue. - rooms[room.id] = room; - } - - // Get the room account data - for (final key in _roomAccountDataBox.keys) { - final roomId = MultiKey.fromString(key).parts.first; - if (rooms.containsKey(roomId)) { - final raw = await _roomAccountDataBox.get(key); - final basicRoomEvent = BasicRoomEvent.fromJson( - convertToJson(raw), - ); - rooms[roomId]!.roomAccountData[basicRoomEvent.type] = - basicRoomEvent; - } else { - Logs().w( - 'Found account data for unknown room $roomId. Delete now...', - ); - await _roomAccountDataBox.delete(key); - } - } - - return rooms.values.toList(); - }, - _roomsBox.keys.length, - ); - - @override - Future getSSSSCache(String type) async { - final raw = await _ssssCacheBox.get(type); - if (raw == null) return null; - return SSSSCache.fromJson(convertToJson(raw)); - } - - @override - Future> getToDeviceEventQueue() async => - await Future.wait( - _toDeviceQueueBox.keys.map((i) async { - final raw = await _toDeviceQueueBox.get(i); - raw['id'] = i; - return QueuedToDeviceEvent.fromJson(convertToJson(raw)); - }).toList(), - ); - - @override - Future> getUnimportantRoomEventStatesForRoom( - List events, - Room room, - ) async { - final keys = _roomStateBox.keys.where((key) { - final tuple = MultiKey.fromString(key); - return tuple.parts.first == room.id && !events.contains(tuple.parts[1]); - }); - - final unimportantEvents = []; - for (final key in keys) { - final Map states = await _roomStateBox.get(key); - unimportantEvents.addAll( - states.values.map((raw) => Event.fromJson(convertToJson(raw), room)), - ); - } - return unimportantEvents.where((event) => event.stateKey != null).toList(); - } - - @override - Future getUser(String userId, Room room) async { - final state = - await _roomMembersBox.get(MultiKey(room.id, userId).toString()); - if (state == null) return null; - return Event.fromJson(convertToJson(state), room).asUser; - } - - @override - Future> getUserDeviceKeys(Client client) => - runBenchmarked>( - 'Get all user device keys from Hive', - () async { - final deviceKeysOutdated = _userDeviceKeysOutdatedBox.keys; - if (deviceKeysOutdated.isEmpty) { - return {}; - } - final res = {}; - for (final userId in deviceKeysOutdated) { - final deviceKeysBoxKeys = _userDeviceKeysBox.keys.where((tuple) { - final tupleKey = MultiKey.fromString(tuple); - return tupleKey.parts.first == userId; - }); - final crossSigningKeysBoxKeys = - _userCrossSigningKeysBox.keys.where((tuple) { - final tupleKey = MultiKey.fromString(tuple); - return tupleKey.parts.first == userId; - }); - res[userId] = DeviceKeysList.fromDbJson( - { - 'client_id': client.id, - 'user_id': userId, - 'outdated': await _userDeviceKeysOutdatedBox.get(userId), - }, - await Future.wait( - deviceKeysBoxKeys.map( - (key) async => - convertToJson(await _userDeviceKeysBox.get(key)), - ), - ), - await Future.wait( - crossSigningKeysBoxKeys.map( - (key) async => - convertToJson(await _userCrossSigningKeysBox.get(key)), - ), - ), - client, - ); - } - return res; - }, - _userDeviceKeysBox.keys.length, - ); - - @override - Future> getUsers(Room room) async { - final users = []; - for (final key in _roomMembersBox.keys) { - final statesKey = MultiKey.fromString(key); - if (statesKey.parts[0] != room.id) continue; - final state = await _roomMembersBox.get(key); - users.add(Event.fromJson(convertToJson(state), room).asUser); - } - return users; - } - - @override - Future insertClient( - String name, - String homeserverUrl, - String token, - DateTime? tokenExpiresAt, - String? refreshToken, - String userId, - String? deviceId, - String? deviceName, - String? prevBatch, - String? olmAccount, - ) async { - await _clientBox.put('homeserver_url', homeserverUrl); - await _clientBox.put('token', token); - await _clientBox.put( - 'token_expires_at', - tokenExpiresAt?.millisecondsSinceEpoch.toString(), - ); - await _clientBox.put('refresh_token', refreshToken); - await _clientBox.put('user_id', userId); - await _clientBox.put('device_id', deviceId); - await _clientBox.put('device_name', deviceName); - await _clientBox.put('prev_batch', prevBatch); - await _clientBox.put('olm_account', olmAccount); - await _clientBox.put('sync_filter_id', null); - return; - } - - @override - Future insertIntoToDeviceQueue( - String type, - String txnId, - String content, - ) async { - return await _toDeviceQueueBox.add({ - 'type': type, - 'txn_id': txnId, - 'content': content, - }); - } - - @override - Future markInboundGroupSessionAsUploaded( - String roomId, - String sessionId, - ) async { - final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey); - if (raw == null) { - Logs().w( - 'Tried to mark inbound group session as uploaded which was not found in the database!', - ); - return; - } - raw['uploaded'] = true; - await _inboundGroupSessionsBox.put(sessionId.toHiveKey, raw); - return; - } - - @override - Future markInboundGroupSessionsAsNeedingUpload() async { - for (final sessionId in _inboundGroupSessionsBox.keys) { - final raw = await _inboundGroupSessionsBox.get(sessionId); - raw['uploaded'] = false; - await _inboundGroupSessionsBox.put(sessionId, raw); - } - return; - } - - @override - Future removeEvent(String eventId, String roomId) async { - await _eventsBox.delete(MultiKey(roomId, eventId).toString()); - for (final key in _timelineFragmentsBox.keys) { - final multiKey = MultiKey.fromString(key); - if (multiKey.parts.first != roomId) continue; - final List eventIds = await _timelineFragmentsBox.get(key) ?? []; - final prevLength = eventIds.length; - eventIds.removeWhere((id) => id == eventId); - if (eventIds.length < prevLength) { - await _timelineFragmentsBox.put(key, eventIds); - } - } - return; - } - - @override - Future removeOutboundGroupSession(String roomId) async { - await _outboundGroupSessionsBox.delete(roomId.toHiveKey); - return; - } - - @override - Future removeUserCrossSigningKey( - String userId, - String publicKey, - ) async { - await _userCrossSigningKeysBox - .delete(MultiKey(userId, publicKey).toString()); - return; - } - - @override - Future removeUserDeviceKey(String userId, String deviceId) async { - await _userDeviceKeysBox.delete(MultiKey(userId, deviceId).toString()); - return; - } - - @override - Future setBlockedUserCrossSigningKey( - bool blocked, - String userId, - String publicKey, - ) async { - final raw = await _userCrossSigningKeysBox - .get(MultiKey(userId, publicKey).toString()); - raw['blocked'] = blocked; - await _userCrossSigningKeysBox.put( - MultiKey(userId, publicKey).toString(), - raw, - ); - return; - } - - @override - Future setBlockedUserDeviceKey( - bool blocked, - String userId, - String deviceId, - ) async { - final raw = - await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString()); - raw['blocked'] = blocked; - await _userDeviceKeysBox.put( - MultiKey(userId, deviceId).toString(), - raw, - ); - return; - } - - @override - Future setLastActiveUserDeviceKey( - int lastActive, - String userId, - String deviceId, - ) async { - final raw = - await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString()); - raw['last_active'] = lastActive; - await _userDeviceKeysBox.put( - MultiKey(userId, deviceId).toString(), - raw, - ); - } - - @override - Future setLastSentMessageUserDeviceKey( - String lastSentMessage, - String userId, - String deviceId, - ) async { - final raw = - await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString()); - raw['last_sent_message'] = lastSentMessage; - await _userDeviceKeysBox.put( - MultiKey(userId, deviceId).toString(), - raw, - ); - } - - @override - Future setRoomPrevBatch( - String? prevBatch, - String roomId, - Client client, - ) async { - final raw = await _roomsBox.get(roomId.toHiveKey); - if (raw == null) return; - final room = Room.fromJson(convertToJson(raw), client); - room.prev_batch = prevBatch; - await _roomsBox.put(roomId.toHiveKey, room.toJson()); - return; - } - - @override - Future setVerifiedUserCrossSigningKey( - bool verified, - String userId, - String publicKey, - ) async { - final raw = (await _userCrossSigningKeysBox - .get(MultiKey(userId, publicKey).toString()) as Map?) ?? - {}; - raw['verified'] = verified; - await _userCrossSigningKeysBox.put( - MultiKey(userId, publicKey).toString(), - raw, - ); - return; - } - - @override - Future setVerifiedUserDeviceKey( - bool verified, - String userId, - String deviceId, - ) async { - final raw = - await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString()); - raw['verified'] = verified; - await _userDeviceKeysBox.put( - MultiKey(userId, deviceId).toString(), - raw, - ); - return; - } - - @override - Future storeAccountData( - String type, - Map content, - ) async { - await _accountDataBox.put( - type.toHiveKey, - convertToJson(content), - ); - return; - } - - @override - Future storeEventUpdate(EventUpdate eventUpdate, Client client) async { - // In case of this is a redaction event - if (eventUpdate.content['type'] == EventTypes.Redaction) { - final tmpRoom = client.getRoomById(eventUpdate.roomID) ?? - Room(id: eventUpdate.roomID, client: client); - final eventId = eventUpdate.content.tryGet('redacts'); - final event = - eventId != null ? await getEventById(eventId, tmpRoom) : null; - if (event != null) { - event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom)); - await _eventsBox.put( - MultiKey(eventUpdate.roomID, event.eventId).toString(), - event.toJson(), - ); - - if (tmpRoom.lastEvent?.eventId == event.eventId) { - await _roomStateBox.put( - MultiKey(eventUpdate.roomID, event.type).toString(), - {'': event.toJson()}, - ); - } - } - } - - // Store a common message event - if ({ - EventUpdateType.timeline, - EventUpdateType.history, - EventUpdateType.decryptedTimelineQueue, - }.contains(eventUpdate.type)) { - final eventId = eventUpdate.content['event_id']; - // Is this ID already in the store? - final Map? prevEvent = await _eventsBox - .get(MultiKey(eventUpdate.roomID, eventId).toString()); - final prevStatus = prevEvent == null - ? null - : () { - final json = convertToJson(prevEvent); - final statusInt = json.tryGet('status') ?? - json - .tryGetMap('unsigned') - ?.tryGet(messageSendingStatusKey); - return statusInt == null ? null : eventStatusFromInt(statusInt); - }(); - - // calculate the status - final newStatus = eventStatusFromInt( - eventUpdate.content.tryGet('status') ?? - eventUpdate.content - .tryGetMap('unsigned') - ?.tryGet(messageSendingStatusKey) ?? - EventStatus.synced.intValue, - ); - - // Is this the response to a sending event which is already synced? Then - // there is nothing to do here. - if (!newStatus.isSynced && prevStatus != null && prevStatus.isSynced) { - return; - } - - final status = newStatus.isError || prevStatus == null - ? newStatus - : latestEventStatus( - prevStatus, - newStatus, - ); - - // Add the status and the sort order to the content so it get stored - eventUpdate.content['unsigned'] ??= {}; - eventUpdate.content['unsigned'][messageSendingStatusKey] = - eventUpdate.content['status'] = status.intValue; - - // In case this event has sent from this account we have a transaction ID - final transactionId = eventUpdate.content - .tryGetMap('unsigned') - ?.tryGet('transaction_id'); - - await _eventsBox.put( - MultiKey(eventUpdate.roomID, eventId).toString(), - eventUpdate.content, - ); - - // Update timeline fragments - final key = MultiKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING') - .toString(); - - final List eventIds = (await _timelineFragmentsBox.get(key) ?? []); - - if (!eventIds.contains(eventId)) { - if (eventUpdate.type == EventUpdateType.history) { - eventIds.add(eventId); - } else { - eventIds.insert(0, eventId); - } - await _timelineFragmentsBox.put(key, eventIds); - } else if (status.isSynced && - prevStatus != null && - prevStatus.isSent && - eventUpdate.type != EventUpdateType.history) { - // Status changes from 1 -> 2? Make sure event is correctly sorted. - eventIds.remove(eventId); - eventIds.insert(0, eventId); - } - - // If event comes from server timeline, remove sending events with this ID - if (status.isSent) { - final key = MultiKey(eventUpdate.roomID, 'SENDING').toString(); - final List eventIds = (await _timelineFragmentsBox.get(key) ?? []); - final i = eventIds.indexWhere((id) => id == eventId); - if (i != -1) { - await _timelineFragmentsBox.put(key, eventIds..removeAt(i)); - } - } - - // Is there a transaction id? Then delete the event with this id. - if (!status.isError && !status.isSending && transactionId != null) { - await removeEvent(transactionId, eventUpdate.roomID); - } - } - - final stateKey = eventUpdate.content['state_key']; - // Store a common state event - if (stateKey != null && - // Don't store events as state updates when paginating backwards. - (eventUpdate.type == EventUpdateType.timeline || - eventUpdate.type == EventUpdateType.state || - eventUpdate.type == EventUpdateType.inviteState)) { - if (eventUpdate.content['type'] == EventTypes.RoomMember) { - await _roomMembersBox.put( - MultiKey( - eventUpdate.roomID, - eventUpdate.content['state_key'], - ).toString(), - eventUpdate.content, - ); - } else { - final key = MultiKey( - eventUpdate.roomID, - eventUpdate.content['type'], - ).toString(); - final Map stateMap = await _roomStateBox.get(key) ?? {}; - - stateMap[stateKey] = eventUpdate.content; - await _roomStateBox.put(key, stateMap); - } - } - - // Store a room account data event - if (eventUpdate.type == EventUpdateType.accountData) { - await _roomAccountDataBox.put( - MultiKey( - eventUpdate.roomID, - eventUpdate.content['type'], - ).toString(), - eventUpdate.content, - ); - } - } - - @override - Future storeFile(Uri mxcUri, Uint8List bytes, int time) async { - return; - } - - @override - Future storeInboundGroupSession( - String roomId, - String sessionId, - String pickle, - String content, - String indexes, - String allowedAtIndex, - String senderKey, - String senderClaimedKey, - ) async { - await _inboundGroupSessionsBox.put( - sessionId.toHiveKey, - StoredInboundGroupSession( - roomId: roomId, - sessionId: sessionId, - pickle: pickle, - content: content, - indexes: indexes, - allowedAtIndex: allowedAtIndex, - senderKey: senderKey, - senderClaimedKeys: senderClaimedKey, - uploaded: false, - ).toJson(), - ); - return; - } - - @override - Future storeOutboundGroupSession( - String roomId, - String pickle, - String deviceIds, - int creationTime, - ) async { - await _outboundGroupSessionsBox.put(roomId.toHiveKey, { - 'room_id': roomId, - 'pickle': pickle, - 'device_ids': deviceIds, - 'creation_time': creationTime, - }); - return; - } - - @override - Future storePrevBatch( - String prevBatch, - ) async { - if (_clientBox.keys.isEmpty) return; - await _clientBox.put('prev_batch', prevBatch); - return; - } - - @override - Future storeRoomUpdate( - String roomId, - SyncRoomUpdate roomUpdate, - Event? lastEvent, - Client client, - ) async { - // Leave room if membership is leave - if (roomUpdate is LeftRoomUpdate) { - await forgetRoom(roomId); - return; - } - final membership = roomUpdate is LeftRoomUpdate - ? Membership.leave - : roomUpdate is InvitedRoomUpdate - ? Membership.invite - : Membership.join; - // Make sure room exists - if (!_roomsBox.containsKey(roomId.toHiveKey)) { - await _roomsBox.put( - roomId.toHiveKey, - roomUpdate is JoinedRoomUpdate - ? Room( - client: client, - id: roomId, - membership: membership, - highlightCount: - roomUpdate.unreadNotifications?.highlightCount?.toInt() ?? - 0, - notificationCount: roomUpdate - .unreadNotifications?.notificationCount - ?.toInt() ?? - 0, - prev_batch: roomUpdate.timeline?.prevBatch, - summary: roomUpdate.summary, - lastEvent: lastEvent, - ).toJson() - : Room( - client: client, - id: roomId, - membership: membership, - lastEvent: lastEvent, - ).toJson(), - ); - } else if (roomUpdate is JoinedRoomUpdate) { - final currentRawRoom = await _roomsBox.get(roomId.toHiveKey); - final currentRoom = Room.fromJson(convertToJson(currentRawRoom), client); - await _roomsBox.put( - roomId.toHiveKey, - Room( - client: client, - id: roomId, - membership: membership, - highlightCount: - roomUpdate.unreadNotifications?.highlightCount?.toInt() ?? - currentRoom.highlightCount, - notificationCount: - roomUpdate.unreadNotifications?.notificationCount?.toInt() ?? - currentRoom.notificationCount, - prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch, - summary: RoomSummary.fromJson( - currentRoom.summary.toJson() - ..addAll(roomUpdate.summary?.toJson() ?? {}), - ), - ).toJson(), - ); - } - } - - @override - Future deleteTimelineForRoom(String roomId) => - _timelineFragmentsBox.delete(TupleKey(roomId, '').toString()); - - @override - Future storeSSSSCache( - String type, - String keyId, - String ciphertext, - String content, - ) async { - await _ssssCacheBox.put( - type, - SSSSCache( - type: type, - keyId: keyId, - ciphertext: ciphertext, - content: content, - ).toJson(), - ); - } - - @override - Future storeSyncFilterId( - String syncFilterId, - ) async { - await _clientBox.put('sync_filter_id', syncFilterId); - } - - @override - Future storeUserCrossSigningKey( - String userId, - String publicKey, - String content, - bool verified, - bool blocked, - ) async { - await _userCrossSigningKeysBox.put( - MultiKey(userId, publicKey).toString(), - { - 'user_id': userId, - 'public_key': publicKey, - 'content': content, - 'verified': verified, - 'blocked': blocked, - }, - ); - } - - @override - Future storeUserDeviceKey( - String userId, - String deviceId, - String content, - bool verified, - bool blocked, - int lastActive, - ) async { - await _userDeviceKeysBox.put(MultiKey(userId, deviceId).toString(), { - 'user_id': userId, - 'device_id': deviceId, - 'content': content, - 'verified': verified, - 'blocked': blocked, - 'last_active': lastActive, - 'last_sent_message': '', - }); - return; - } - - @override - Future storeUserDeviceKeysInfo(String userId, bool outdated) async { - await _userDeviceKeysOutdatedBox.put(userId.toHiveKey, outdated); - return; - } - - @override - Future transaction(Future Function() action) => - zoneTransaction(action); - - @override - Future updateClient( - String homeserverUrl, - String token, - DateTime? tokenExpiresAt, - String? refreshToken, - String userId, - String? deviceId, - String? deviceName, - String? prevBatch, - String? olmAccount, - ) async { - await _clientBox.put('homeserver_url', homeserverUrl); - await _clientBox.put('token', token); - await _clientBox.put( - 'token_expires_at', - tokenExpiresAt?.millisecondsSinceEpoch.toString(), - ); - await _clientBox.put('refresh_token', refreshToken); - await _clientBox.put('user_id', userId); - await _clientBox.put('device_id', deviceId); - await _clientBox.put('device_name', deviceName); - await _clientBox.put('prev_batch', prevBatch); - await _clientBox.put('olm_account', olmAccount); - return; - } - - @override - Future updateClientKeys( - String olmAccount, - ) async { - await _clientBox.put('olm_account', olmAccount); - return; - } - - @override - Future updateInboundGroupSessionAllowedAtIndex( - String allowedAtIndex, - String roomId, - String sessionId, - ) async { - final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey); - if (raw == null) { - Logs().w( - 'Tried to update inbound group session as uploaded which wasnt found in the database!', - ); - return; - } - raw['allowed_at_index'] = allowedAtIndex; - await _inboundGroupSessionsBox.put(sessionId.toHiveKey, raw); - return; - } - - @override - Future updateInboundGroupSessionIndexes( - String indexes, - String roomId, - String sessionId, - ) async { - final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey); - if (raw == null) { - Logs().w( - 'Tried to update inbound group session indexes of a session which was not found in the database!', - ); - return; - } - raw['indexes'] = indexes; - await _inboundGroupSessionsBox.put(sessionId.toHiveKey, raw); - return; - } - - @override - Future> getAllInboundGroupSessions() async { - final rawSessions = await Future.wait( - _inboundGroupSessionsBox.keys - .map((key) => _inboundGroupSessionsBox.get(key)), - ); - return rawSessions - .map((raw) => StoredInboundGroupSession.fromJson(convertToJson(raw))) - .toList(); - } - - @override - Future addSeenDeviceId( - String userId, - String deviceId, - String publicKeys, - ) => - _seenDeviceIdsBox.put(MultiKey(userId, deviceId).toString(), publicKeys); - - @override - Future addSeenPublicKey( - String publicKey, - String deviceId, - ) => - _seenDeviceKeysBox.put(publicKey.toHiveKey, deviceId); - - @override - Future deviceIdSeen(userId, deviceId) async { - final raw = - await _seenDeviceIdsBox.get(MultiKey(userId, deviceId).toString()); - if (raw == null) return null; - return raw as String; - } - - @override - Future publicKeySeen(String publicKey) async { - final raw = await _seenDeviceKeysBox.get(publicKey.toHiveKey); - if (raw == null) return null; - return raw as String; - } - - @override - Future storePresence(String userId, CachedPresence presence) => - _presencesBox.put(userId, presence.toJson()); - - @override - Future getPresence(String userId) async { - final rawPresence = await _presencesBox.get(userId); - if (rawPresence == null) return null; - - return CachedPresence.fromJson(copyMap(rawPresence)); - } - - @override - Future exportDump() { - // see no need to implement this in a deprecated part - throw UnimplementedError(); - } - - @override - Future importDump(String export) { - // see no need to implement this in a deprecated part - throw UnimplementedError(); - } - - @override - Future storeWellKnown(DiscoveryInformation? discoveryInformation) { - if (discoveryInformation == null) { - return _clientBox.delete('discovery_information'); - } - return _clientBox.put( - 'discovery_information', - jsonEncode(discoveryInformation.toJson()), - ); - } - - @override - Future getWellKnown() async { - final rawDiscoveryInformation = - await _clientBox.get('discovery_information'); - if (rawDiscoveryInformation == null) return null; - return DiscoveryInformation.fromJson(jsonDecode(rawDiscoveryInformation)); - } - - @override - Future delete() => Hive.deleteFromDisk(); - - @override - Future markUserProfileAsOutdated(userId) async { - return; - } - - @override - Future getUserProfile(String userId) async { - return null; - } - - @override - Future storeUserProfile( - String userId, - CachedProfileInformation profile, - ) async { - return; - } -} - -dynamic _castValue(dynamic value) { - if (value is Map) { - return convertToJson(value); - } - if (value is List) { - return value.map(_castValue).toList(); - } - return value; -} - -/// Hive always gives back an `_InternalLinkedHasMap`. This -/// creates a deep copy of the json and makes sure that the format is always -/// `Map`. -Map convertToJson(Map map) { - final copy = Map.from(map); - for (final entry in copy.entries) { - copy[entry.key] = _castValue(entry.value); - } - return copy; -} - -class MultiKey { - final List parts; - - MultiKey(String key1, [String? key2, String? key3]) - : parts = [ - key1, - if (key2 != null) key2, - if (key3 != null) key3, - ]; - - const MultiKey.byParts(this.parts); - - MultiKey.fromString(String multiKeyString) - : parts = multiKeyString.split('|').map((s) => s.fromHiveKey).toList(); - - @override - String toString() => parts.map((s) => s.toHiveKey).join('|'); - - @override - bool operator ==(other) => parts.toString() == other.toString(); - - @override - int get hashCode => Object.hashAll(parts); -} - -extension HiveKeyExtension on String { - String get toHiveKey => isValidMatrixId - ? '$sigil${Uri.encodeComponent(localpart!)}:${Uri.encodeComponent(domain!)}' - : Uri.encodeComponent(this); -} - -extension FromHiveKeyExtension on String { - String get fromHiveKey => Uri.decodeComponent(this); -} diff --git a/test/database_api_test.dart b/test/database_api_test.dart index 4be5e81d..2846a2fb 100644 --- a/test/database_api_test.dart +++ b/test/database_api_test.dart @@ -38,7 +38,6 @@ String createLargeString(String character, int desiredSize) { void main() { final databaseBuilders = { 'Matrix SDK Database': getMatrixSdkDatabase, - 'Hive Database': getHiveDatabase, 'Hive Collections Database': getHiveCollectionsDatabase, }; @@ -660,9 +659,7 @@ void main() { ), ); // ignore: deprecated_member_use_from_same_package - if (database is! HiveCollectionsDatabase && - // ignore: deprecated_member_use_from_same_package - database is! FamedlySdkHiveDatabase) { + if (database is! HiveCollectionsDatabase) { final profile2 = await database.getUserProfile('@alice:example.com'); expect(profile2?.displayname, 'Alice M'); diff --git a/test/fake_database.dart b/test/fake_database.dart index c0cdcdfd..6ebe13e1 100644 --- a/test/fake_database.dart +++ b/test/fake_database.dart @@ -61,18 +61,3 @@ Future getMatrixSdkDatabase( await db.open(); return db; } - -// ignore: deprecated_member_use_from_same_package -Future getHiveDatabase(Client? c) async { - if (!hiveInitialized) { - final testHivePath = await LocalFileSystem() - .systemTempDirectory - .createTemp('dart-sdk-tests-database'); - Hive.init(testHivePath.path); - hiveInitialized = true; - } - // ignore: deprecated_member_use_from_same_package - final db = FamedlySdkHiveDatabase('unit_test.${c?.hashCode}'); - await db.open(); - return db; -}