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 : }
|