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