/* * 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); }