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 :
23 : import 'package:sqflite_common/sqflite.dart';
24 :
25 : import 'package:matrix/encryption/utils/olm_session.dart';
26 : import 'package:matrix/encryption/utils/outbound_group_session.dart';
27 : import 'package:matrix/encryption/utils/ssss_cache.dart';
28 : import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
29 : import 'package:matrix/matrix.dart';
30 : import 'package:matrix/src/utils/copy_map.dart';
31 : import 'package:matrix/src/utils/queued_to_device_event.dart';
32 : import 'package:matrix/src/utils/run_benchmarked.dart';
33 :
34 : import 'package:matrix/src/database/indexeddb_box.dart'
35 : if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart';
36 :
37 : import 'package:matrix/src/database/database_file_storage_stub.dart'
38 : if (dart.library.io) 'package:matrix/src/database/database_file_storage_io.dart';
39 :
40 : /// Database based on SQlite3 on native and IndexedDB on web. For native you
41 : /// have to pass a `Database` object, which can be created with the sqflite
42 : /// package like this:
43 : /// ```dart
44 : /// final database = await openDatabase('path/to/your/database');
45 : /// ```
46 : ///
47 : /// **WARNING**: For android it seems like that the CursorWindow is too small for
48 : /// large amounts of data if you are using SQFlite. Consider using a different
49 : /// package to open the database like
50 : /// [sqflite_sqlcipher](https://pub.dev/packages/sqflite_sqlcipher) or
51 : /// [sqflite_common_ffi](https://pub.dev/packages/sqflite_common_ffi).
52 : /// Learn more at:
53 : /// https://github.com/famedly/matrix-dart-sdk/issues/1642#issuecomment-1865827227
54 : class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
55 : static const int version = 10;
56 : final String name;
57 :
58 : late BoxCollection _collection;
59 : late Box<String> _clientBox;
60 : late Box<Map> _accountDataBox;
61 : late Box<Map> _roomsBox;
62 : late Box<Map> _toDeviceQueueBox;
63 :
64 : /// Key is a tuple as TupleKey(roomId, type, stateKey) where stateKey can be
65 : /// an empty string. Must contain only states of type
66 : /// client.importantRoomStates.
67 : late Box<Map> _preloadRoomStateBox;
68 :
69 : /// Key is a tuple as TupleKey(roomId, type, stateKey) where stateKey can be
70 : /// an empty string. Must NOT contain states of a type from
71 : /// client.importantRoomStates.
72 : late Box<Map> _nonPreloadRoomStateBox;
73 :
74 : /// Key is a tuple as TupleKey(roomId, userId)
75 : late Box<Map> _roomMembersBox;
76 :
77 : /// Key is a tuple as TupleKey(roomId, type)
78 : late Box<Map> _roomAccountDataBox;
79 : late Box<Map> _inboundGroupSessionsBox;
80 : late Box<String> _inboundGroupSessionsUploadQueueBox;
81 : late Box<Map> _outboundGroupSessionsBox;
82 : late Box<Map> _olmSessionsBox;
83 :
84 : /// Key is a tuple as TupleKey(userId, deviceId)
85 : late Box<Map> _userDeviceKeysBox;
86 :
87 : /// Key is the user ID as a String
88 : late Box<bool> _userDeviceKeysOutdatedBox;
89 :
90 : /// Key is a tuple as TupleKey(userId, publicKey)
91 : late Box<Map> _userCrossSigningKeysBox;
92 : late Box<Map> _ssssCacheBox;
93 : late Box<Map> _presencesBox;
94 :
95 : /// Key is a tuple as Multikey(roomId, fragmentId) while the default
96 : /// fragmentId is an empty String
97 : late Box<List> _timelineFragmentsBox;
98 :
99 : /// Key is a tuple as TupleKey(roomId, eventId)
100 : late Box<Map> _eventsBox;
101 :
102 : /// Key is a tuple as TupleKey(userId, deviceId)
103 : late Box<String> _seenDeviceIdsBox;
104 :
105 : late Box<String> _seenDeviceKeysBox;
106 :
107 : late Box<Map> _userProfilesBox;
108 :
109 : @override
110 : final int maxFileSize;
111 :
112 : // there was a field of type `dart:io:Directory` here. This one broke the
113 : // dart js standalone compiler. Migration via URI as file system identifier.
114 0 : @Deprecated(
115 : 'Breaks support for web standalone. Use [fileStorageLocation] instead.',
116 : )
117 0 : Object? get fileStoragePath => fileStorageLocation?.toFilePath();
118 :
119 : static const String _clientBoxName = 'box_client';
120 :
121 : static const String _accountDataBoxName = 'box_account_data';
122 :
123 : static const String _roomsBoxName = 'box_rooms';
124 :
125 : static const String _toDeviceQueueBoxName = 'box_to_device_queue';
126 :
127 : static const String _preloadRoomStateBoxName = 'box_preload_room_states';
128 :
129 : static const String _nonPreloadRoomStateBoxName =
130 : 'box_non_preload_room_states';
131 :
132 : static const String _roomMembersBoxName = 'box_room_members';
133 :
134 : static const String _roomAccountDataBoxName = 'box_room_account_data';
135 :
136 : static const String _inboundGroupSessionsBoxName =
137 : 'box_inbound_group_session';
138 :
139 : static const String _inboundGroupSessionsUploadQueueBoxName =
140 : 'box_inbound_group_sessions_upload_queue';
141 :
142 : static const String _outboundGroupSessionsBoxName =
143 : 'box_outbound_group_session';
144 :
145 : static const String _olmSessionsBoxName = 'box_olm_session';
146 :
147 : static const String _userDeviceKeysBoxName = 'box_user_device_keys';
148 :
149 : static const String _userDeviceKeysOutdatedBoxName =
150 : 'box_user_device_keys_outdated';
151 :
152 : static const String _userCrossSigningKeysBoxName = 'box_cross_signing_keys';
153 :
154 : static const String _ssssCacheBoxName = 'box_ssss_cache';
155 :
156 : static const String _presencesBoxName = 'box_presences';
157 :
158 : static const String _timelineFragmentsBoxName = 'box_timeline_fragments';
159 :
160 : static const String _eventsBoxName = 'box_events';
161 :
162 : static const String _seenDeviceIdsBoxName = 'box_seen_device_ids';
163 :
164 : static const String _seenDeviceKeysBoxName = 'box_seen_device_keys';
165 :
166 : static const String _userProfilesBoxName = 'box_user_profiles';
167 :
168 : Database? database;
169 :
170 : /// Custom IdbFactory used to create the indexedDB. On IO platforms it would
171 : /// lead to an error to import "dart:indexed_db" so this is dynamically
172 : /// typed.
173 : final dynamic idbFactory;
174 :
175 : /// Custom SQFlite Database Factory used for high level operations on IO
176 : /// like delete. Set it if you want to use sqlite FFI.
177 : final DatabaseFactory? sqfliteFactory;
178 :
179 34 : MatrixSdkDatabase(
180 : this.name, {
181 : this.database,
182 : this.idbFactory,
183 : this.sqfliteFactory,
184 : this.maxFileSize = 0,
185 : // TODO : remove deprecated member migration on next major release
186 : @Deprecated(
187 : 'Breaks support for web standalone. Use [fileStorageLocation] instead.',
188 : )
189 : dynamic fileStoragePath,
190 : Uri? fileStorageLocation,
191 : Duration? deleteFilesAfterDuration,
192 : }) {
193 0 : final legacyPath = fileStoragePath?.path;
194 34 : this.fileStorageLocation = fileStorageLocation ??
195 34 : (legacyPath is String ? Uri.tryParse(legacyPath) : null);
196 34 : this.deleteFilesAfterDuration = deleteFilesAfterDuration;
197 : }
198 :
199 34 : Future<void> open() async {
200 68 : _collection = await BoxCollection.open(
201 34 : name,
202 : {
203 34 : _clientBoxName,
204 34 : _accountDataBoxName,
205 34 : _roomsBoxName,
206 34 : _toDeviceQueueBoxName,
207 34 : _preloadRoomStateBoxName,
208 34 : _nonPreloadRoomStateBoxName,
209 34 : _roomMembersBoxName,
210 34 : _roomAccountDataBoxName,
211 34 : _inboundGroupSessionsBoxName,
212 34 : _inboundGroupSessionsUploadQueueBoxName,
213 34 : _outboundGroupSessionsBoxName,
214 34 : _olmSessionsBoxName,
215 34 : _userDeviceKeysBoxName,
216 34 : _userDeviceKeysOutdatedBoxName,
217 34 : _userCrossSigningKeysBoxName,
218 34 : _ssssCacheBoxName,
219 34 : _presencesBoxName,
220 34 : _timelineFragmentsBoxName,
221 34 : _eventsBoxName,
222 34 : _seenDeviceIdsBoxName,
223 34 : _seenDeviceKeysBoxName,
224 34 : _userProfilesBoxName,
225 : },
226 34 : sqfliteDatabase: database,
227 34 : sqfliteFactory: sqfliteFactory,
228 34 : idbFactory: idbFactory,
229 : version: version,
230 : );
231 102 : _clientBox = _collection.openBox<String>(
232 : _clientBoxName,
233 : );
234 102 : _accountDataBox = _collection.openBox<Map>(
235 : _accountDataBoxName,
236 : );
237 102 : _roomsBox = _collection.openBox<Map>(
238 : _roomsBoxName,
239 : );
240 102 : _preloadRoomStateBox = _collection.openBox(
241 : _preloadRoomStateBoxName,
242 : );
243 102 : _nonPreloadRoomStateBox = _collection.openBox(
244 : _nonPreloadRoomStateBoxName,
245 : );
246 102 : _roomMembersBox = _collection.openBox(
247 : _roomMembersBoxName,
248 : );
249 102 : _toDeviceQueueBox = _collection.openBox(
250 : _toDeviceQueueBoxName,
251 : );
252 102 : _roomAccountDataBox = _collection.openBox(
253 : _roomAccountDataBoxName,
254 : );
255 102 : _inboundGroupSessionsBox = _collection.openBox(
256 : _inboundGroupSessionsBoxName,
257 : );
258 102 : _inboundGroupSessionsUploadQueueBox = _collection.openBox(
259 : _inboundGroupSessionsUploadQueueBoxName,
260 : );
261 102 : _outboundGroupSessionsBox = _collection.openBox(
262 : _outboundGroupSessionsBoxName,
263 : );
264 102 : _olmSessionsBox = _collection.openBox(
265 : _olmSessionsBoxName,
266 : );
267 102 : _userDeviceKeysBox = _collection.openBox(
268 : _userDeviceKeysBoxName,
269 : );
270 102 : _userDeviceKeysOutdatedBox = _collection.openBox(
271 : _userDeviceKeysOutdatedBoxName,
272 : );
273 102 : _userCrossSigningKeysBox = _collection.openBox(
274 : _userCrossSigningKeysBoxName,
275 : );
276 102 : _ssssCacheBox = _collection.openBox(
277 : _ssssCacheBoxName,
278 : );
279 102 : _presencesBox = _collection.openBox(
280 : _presencesBoxName,
281 : );
282 102 : _timelineFragmentsBox = _collection.openBox(
283 : _timelineFragmentsBoxName,
284 : );
285 102 : _eventsBox = _collection.openBox(
286 : _eventsBoxName,
287 : );
288 102 : _seenDeviceIdsBox = _collection.openBox(
289 : _seenDeviceIdsBoxName,
290 : );
291 102 : _seenDeviceKeysBox = _collection.openBox(
292 : _seenDeviceKeysBoxName,
293 : );
294 102 : _userProfilesBox = _collection.openBox(
295 : _userProfilesBoxName,
296 : );
297 :
298 : // Check version and check if we need a migration
299 102 : final currentVersion = int.tryParse(await _clientBox.get('version') ?? '');
300 : if (currentVersion == null) {
301 102 : await _clientBox.put('version', version.toString());
302 0 : } else if (currentVersion != version) {
303 0 : await _migrateFromVersion(currentVersion);
304 : }
305 :
306 : return;
307 : }
308 :
309 0 : Future<void> _migrateFromVersion(int currentVersion) async {
310 0 : Logs().i('Migrate store database from version $currentVersion to $version');
311 :
312 0 : if (version == 8) {
313 : // Migrate to inbound group sessions upload queue:
314 0 : final allInboundGroupSessions = await getAllInboundGroupSessions();
315 : final sessionsToUpload = allInboundGroupSessions
316 : // ignore: deprecated_member_use_from_same_package
317 0 : .where((session) => session.uploaded == false)
318 0 : .toList();
319 0 : Logs().i(
320 0 : 'Move ${allInboundGroupSessions.length} inbound group sessions to upload to their own queue...',
321 : );
322 0 : await transaction(() async {
323 0 : for (final session in sessionsToUpload) {
324 0 : await _inboundGroupSessionsUploadQueueBox.put(
325 0 : session.sessionId,
326 0 : session.roomId,
327 : );
328 : }
329 : });
330 0 : if (currentVersion == 7) {
331 0 : await _clientBox.put('version', version.toString());
332 : return;
333 : }
334 : }
335 : // The default version upgrade:
336 0 : await clearCache();
337 0 : await _clientBox.put('version', version.toString());
338 : }
339 :
340 9 : @override
341 : Future<void> clear() async {
342 18 : _clientBox.clearQuickAccessCache();
343 18 : _accountDataBox.clearQuickAccessCache();
344 18 : _roomsBox.clearQuickAccessCache();
345 18 : _preloadRoomStateBox.clearQuickAccessCache();
346 18 : _nonPreloadRoomStateBox.clearQuickAccessCache();
347 18 : _roomMembersBox.clearQuickAccessCache();
348 18 : _toDeviceQueueBox.clearQuickAccessCache();
349 18 : _roomAccountDataBox.clearQuickAccessCache();
350 18 : _inboundGroupSessionsBox.clearQuickAccessCache();
351 18 : _inboundGroupSessionsUploadQueueBox.clearQuickAccessCache();
352 18 : _outboundGroupSessionsBox.clearQuickAccessCache();
353 18 : _olmSessionsBox.clearQuickAccessCache();
354 18 : _userDeviceKeysBox.clearQuickAccessCache();
355 18 : _userDeviceKeysOutdatedBox.clearQuickAccessCache();
356 18 : _userCrossSigningKeysBox.clearQuickAccessCache();
357 18 : _ssssCacheBox.clearQuickAccessCache();
358 18 : _presencesBox.clearQuickAccessCache();
359 18 : _timelineFragmentsBox.clearQuickAccessCache();
360 18 : _eventsBox.clearQuickAccessCache();
361 18 : _seenDeviceIdsBox.clearQuickAccessCache();
362 18 : _seenDeviceKeysBox.clearQuickAccessCache();
363 18 : _userProfilesBox.clearQuickAccessCache();
364 :
365 18 : await _collection.clear();
366 : }
367 :
368 3 : @override
369 6 : Future<void> clearCache() => transaction(() async {
370 6 : await _roomsBox.clear();
371 6 : await _accountDataBox.clear();
372 6 : await _roomAccountDataBox.clear();
373 6 : await _preloadRoomStateBox.clear();
374 6 : await _nonPreloadRoomStateBox.clear();
375 6 : await _roomMembersBox.clear();
376 6 : await _eventsBox.clear();
377 6 : await _timelineFragmentsBox.clear();
378 6 : await _outboundGroupSessionsBox.clear();
379 6 : await _presencesBox.clear();
380 6 : await _userProfilesBox.clear();
381 6 : await _clientBox.delete('prev_batch');
382 : });
383 :
384 4 : @override
385 8 : Future<void> clearSSSSCache() => _ssssCacheBox.clear();
386 :
387 21 : @override
388 42 : Future<void> close() async => _collection.close();
389 :
390 2 : @override
391 : Future<void> deleteFromToDeviceQueue(int id) async {
392 6 : await _toDeviceQueueBox.delete(id.toString());
393 : return;
394 : }
395 :
396 32 : @override
397 : Future<void> forgetRoom(String roomId) async {
398 128 : await _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
399 64 : final eventsBoxKeys = await _eventsBox.getAllKeys();
400 32 : for (final key in eventsBoxKeys) {
401 0 : final multiKey = TupleKey.fromString(key);
402 0 : if (multiKey.parts.first != roomId) continue;
403 0 : await _eventsBox.delete(key);
404 : }
405 64 : final preloadRoomStateBoxKeys = await _preloadRoomStateBox.getAllKeys();
406 34 : for (final key in preloadRoomStateBoxKeys) {
407 2 : final multiKey = TupleKey.fromString(key);
408 6 : if (multiKey.parts.first != roomId) continue;
409 0 : await _preloadRoomStateBox.delete(key);
410 : }
411 : final nonPreloadRoomStateBoxKeys =
412 64 : await _nonPreloadRoomStateBox.getAllKeys();
413 32 : for (final key in nonPreloadRoomStateBoxKeys) {
414 0 : final multiKey = TupleKey.fromString(key);
415 0 : if (multiKey.parts.first != roomId) continue;
416 0 : await _nonPreloadRoomStateBox.delete(key);
417 : }
418 64 : final roomMembersBoxKeys = await _roomMembersBox.getAllKeys();
419 34 : for (final key in roomMembersBoxKeys) {
420 2 : final multiKey = TupleKey.fromString(key);
421 6 : if (multiKey.parts.first != roomId) continue;
422 0 : await _roomMembersBox.delete(key);
423 : }
424 64 : final roomAccountDataBoxKeys = await _roomAccountDataBox.getAllKeys();
425 32 : for (final key in roomAccountDataBoxKeys) {
426 0 : final multiKey = TupleKey.fromString(key);
427 0 : if (multiKey.parts.first != roomId) continue;
428 0 : await _roomAccountDataBox.delete(key);
429 : }
430 64 : await _roomsBox.delete(roomId);
431 : }
432 :
433 32 : @override
434 : Future<Map<String, BasicEvent>> getAccountData() =>
435 32 : runBenchmarked<Map<String, BasicEvent>>('Get all account data from store',
436 32 : () async {
437 32 : final accountData = <String, BasicEvent>{};
438 64 : final raws = await _accountDataBox.getAllValues();
439 34 : for (final entry in raws.entries) {
440 6 : accountData[entry.key] = BasicEvent(
441 2 : type: entry.key,
442 4 : content: copyMap(entry.value),
443 : );
444 : }
445 : return accountData;
446 : });
447 :
448 32 : @override
449 : Future<Map<String, dynamic>?> getClient(String name) =>
450 64 : runBenchmarked('Get Client from store', () async {
451 32 : final map = <String, dynamic>{};
452 64 : final keys = await _clientBox.getAllKeys();
453 64 : for (final key in keys) {
454 32 : if (key == 'version') continue;
455 4 : final value = await _clientBox.get(key);
456 2 : if (value != null) map[key] = value;
457 : }
458 32 : if (map.isEmpty) return null;
459 : return map;
460 : });
461 :
462 8 : @override
463 : Future<Event?> getEventById(String eventId, Room room) async {
464 40 : final raw = await _eventsBox.get(TupleKey(room.id, eventId).toString());
465 : if (raw == null) return null;
466 12 : return Event.fromJson(copyMap(raw), room);
467 : }
468 :
469 : /// Loads a whole list of events at once from the store for a specific room
470 6 : Future<List<Event>> _getEventsByIds(List<String> eventIds, Room room) async {
471 : final keys = eventIds
472 6 : .map(
473 12 : (eventId) => TupleKey(room.id, eventId).toString(),
474 : )
475 6 : .toList();
476 12 : final rawEvents = await _eventsBox.getAll(keys);
477 : return rawEvents
478 6 : .whereType<Map>()
479 15 : .map((rawEvent) => Event.fromJson(copyMap(rawEvent), room))
480 6 : .toList();
481 : }
482 :
483 6 : @override
484 : Future<List<Event>> getEventList(
485 : Room room, {
486 : int start = 0,
487 : bool onlySending = false,
488 : int? limit,
489 : }) =>
490 12 : runBenchmarked<List<Event>>('Get event list', () async {
491 : // Get the synced event IDs from the store
492 18 : final timelineKey = TupleKey(room.id, '').toString();
493 : final timelineEventIds =
494 15 : (await _timelineFragmentsBox.get(timelineKey) ?? []);
495 :
496 : // Get the local stored SENDING events from the store
497 : late final List sendingEventIds;
498 6 : if (start != 0) {
499 2 : sendingEventIds = [];
500 : } else {
501 18 : final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
502 : sendingEventIds =
503 16 : (await _timelineFragmentsBox.get(sendingTimelineKey) ?? []);
504 : }
505 :
506 : // Combine those two lists while respecting the start and limit parameters.
507 6 : final end = min(
508 6 : timelineEventIds.length,
509 8 : start + (limit ?? timelineEventIds.length),
510 : );
511 6 : final eventIds = [
512 : ...sendingEventIds,
513 10 : if (!onlySending && start < timelineEventIds.length)
514 3 : ...timelineEventIds.getRange(start, end),
515 : ];
516 :
517 12 : return await _getEventsByIds(eventIds.cast<String>(), room);
518 : });
519 :
520 11 : @override
521 : Future<StoredInboundGroupSession?> getInboundGroupSession(
522 : String roomId,
523 : String sessionId,
524 : ) async {
525 22 : final raw = await _inboundGroupSessionsBox.get(sessionId);
526 : if (raw == null) return null;
527 16 : return StoredInboundGroupSession.fromJson(copyMap(raw));
528 : }
529 :
530 6 : @override
531 : Future<List<StoredInboundGroupSession>>
532 : getInboundGroupSessionsToUpload() async {
533 : final uploadQueue =
534 12 : await _inboundGroupSessionsUploadQueueBox.getAllValues();
535 6 : final sessionFutures = uploadQueue.entries
536 6 : .take(50)
537 26 : .map((entry) => getInboundGroupSession(entry.value, entry.key));
538 6 : final sessions = await Future.wait(sessionFutures);
539 12 : return sessions.whereType<StoredInboundGroupSession>().toList();
540 : }
541 :
542 2 : @override
543 : Future<List<String>> getLastSentMessageUserDeviceKey(
544 : String userId,
545 : String deviceId,
546 : ) async {
547 : final raw =
548 8 : await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
549 1 : if (raw == null) return <String>[];
550 2 : return <String>[raw['last_sent_message']];
551 : }
552 :
553 24 : @override
554 : Future<void> storeOlmSession(
555 : String identityKey,
556 : String sessionId,
557 : String pickle,
558 : int lastReceived,
559 : ) async {
560 96 : final rawSessions = copyMap((await _olmSessionsBox.get(identityKey)) ?? {});
561 48 : rawSessions[sessionId] = {
562 : 'identity_key': identityKey,
563 : 'pickle': pickle,
564 : 'session_id': sessionId,
565 : 'last_received': lastReceived,
566 : };
567 48 : await _olmSessionsBox.put(identityKey, rawSessions);
568 : return;
569 : }
570 :
571 24 : @override
572 : Future<List<OlmSession>> getOlmSessions(
573 : String identityKey,
574 : String userId,
575 : ) async {
576 48 : final rawSessions = await _olmSessionsBox.get(identityKey);
577 29 : if (rawSessions == null || rawSessions.isEmpty) return <OlmSession>[];
578 5 : return rawSessions.values
579 20 : .map((json) => OlmSession.fromJson(copyMap(json), userId))
580 5 : .toList();
581 : }
582 :
583 2 : @override
584 : Future<Map<String, Map>> getAllOlmSessions() =>
585 4 : _olmSessionsBox.getAllValues();
586 :
587 11 : @override
588 : Future<List<OlmSession>> getOlmSessionsForDevices(
589 : List<String> identityKeys,
590 : String userId,
591 : ) async {
592 11 : final sessions = await Future.wait(
593 33 : identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)),
594 : );
595 33 : return <OlmSession>[for (final sublist in sessions) ...sublist];
596 : }
597 :
598 4 : @override
599 : Future<OutboundGroupSession?> getOutboundGroupSession(
600 : String roomId,
601 : String userId,
602 : ) async {
603 8 : final raw = await _outboundGroupSessionsBox.get(roomId);
604 : if (raw == null) return null;
605 4 : return OutboundGroupSession.fromJson(copyMap(raw), userId);
606 : }
607 :
608 4 : @override
609 : Future<Room?> getSingleRoom(
610 : Client client,
611 : String roomId, {
612 : bool loadImportantStates = true,
613 : }) async {
614 : // Get raw room from database:
615 8 : final roomData = await _roomsBox.get(roomId);
616 : if (roomData == null) return null;
617 8 : final room = Room.fromJson(copyMap(roomData), client);
618 :
619 : // Get the room account data
620 8 : final allKeys = await _roomAccountDataBox.getAllKeys();
621 : final roomAccountDataKeys = allKeys
622 24 : .where((key) => TupleKey.fromString(key).parts.first == roomId)
623 4 : .toList();
624 : final roomAccountDataList =
625 8 : await _roomAccountDataBox.getAll(roomAccountDataKeys);
626 :
627 8 : for (final data in roomAccountDataList) {
628 : if (data == null) continue;
629 8 : final event = BasicEvent.fromJson(copyMap(data));
630 12 : room.roomAccountData[event.type] = event;
631 : }
632 :
633 : // Get important states:
634 : if (loadImportantStates) {
635 8 : final preloadRoomStateKeys = await _preloadRoomStateBox.getAllKeys();
636 : final keysForRoom = preloadRoomStateKeys
637 24 : .where((key) => TupleKey.fromString(key).parts.first == roomId)
638 4 : .toList();
639 8 : final rawStates = await _preloadRoomStateBox.getAll(keysForRoom);
640 :
641 5 : for (final raw in rawStates) {
642 : if (raw == null) continue;
643 3 : room.setState(Event.fromJson(copyMap(raw), room));
644 : }
645 : }
646 :
647 : return room;
648 : }
649 :
650 32 : @override
651 : Future<List<Room>> getRoomList(Client client) =>
652 64 : runBenchmarked<List<Room>>('Get room list from store', () async {
653 32 : final rooms = <String, Room>{};
654 :
655 64 : final rawRooms = await _roomsBox.getAllValues();
656 :
657 34 : for (final raw in rawRooms.values) {
658 : // Get the room
659 4 : final room = Room.fromJson(copyMap(raw), client);
660 :
661 : // Add to the list and continue.
662 4 : rooms[room.id] = room;
663 : }
664 :
665 64 : final roomStatesDataRaws = await _preloadRoomStateBox.getAllValues();
666 33 : for (final entry in roomStatesDataRaws.entries) {
667 2 : final keys = TupleKey.fromString(entry.key);
668 2 : final roomId = keys.parts.first;
669 1 : final room = rooms[roomId];
670 : if (room == null) {
671 0 : Logs().w('Found event in store for unknown room', entry.value);
672 : continue;
673 : }
674 1 : final raw = entry.value;
675 1 : room.setState(
676 2 : room.membership == Membership.invite
677 2 : ? StrippedStateEvent.fromJson(copyMap(raw))
678 2 : : Event.fromJson(copyMap(raw), room),
679 : );
680 : }
681 :
682 : // Get the room account data
683 64 : final roomAccountDataRaws = await _roomAccountDataBox.getAllValues();
684 33 : for (final entry in roomAccountDataRaws.entries) {
685 2 : final keys = TupleKey.fromString(entry.key);
686 1 : final basicRoomEvent = BasicEvent.fromJson(
687 2 : copyMap(entry.value),
688 : );
689 2 : final roomId = keys.parts.first;
690 1 : if (rooms.containsKey(roomId)) {
691 4 : rooms[roomId]!.roomAccountData[basicRoomEvent.type] =
692 : basicRoomEvent;
693 : } else {
694 0 : Logs().w(
695 0 : 'Found account data for unknown room $roomId. Delete now...',
696 : );
697 0 : await _roomAccountDataBox
698 0 : .delete(TupleKey(roomId, basicRoomEvent.type).toString());
699 : }
700 : }
701 :
702 64 : return rooms.values.toList();
703 : });
704 :
705 24 : @override
706 : Future<SSSSCache?> getSSSSCache(String type) async {
707 48 : final raw = await _ssssCacheBox.get(type);
708 : if (raw == null) return null;
709 16 : return SSSSCache.fromJson(copyMap(raw));
710 : }
711 :
712 32 : @override
713 : Future<List<QueuedToDeviceEvent>> getToDeviceEventQueue() async {
714 64 : final raws = await _toDeviceQueueBox.getAllValues();
715 66 : final copiedRaws = raws.entries.map((entry) {
716 4 : final copiedRaw = copyMap(entry.value);
717 6 : copiedRaw['id'] = int.parse(entry.key);
718 6 : copiedRaw['content'] = jsonDecode(copiedRaw['content'] as String);
719 : return copiedRaw;
720 32 : }).toList();
721 68 : return copiedRaws.map((raw) => QueuedToDeviceEvent.fromJson(raw)).toList();
722 : }
723 :
724 6 : @override
725 : Future<List<Event>> getUnimportantRoomEventStatesForRoom(
726 : List<String> events,
727 : Room room,
728 : ) async {
729 21 : final keys = (await _nonPreloadRoomStateBox.getAllKeys()).where((key) {
730 3 : final tuple = TupleKey.fromString(key);
731 21 : return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
732 : });
733 :
734 6 : final unimportantEvents = <Event>[];
735 9 : for (final key in keys) {
736 6 : final raw = await _nonPreloadRoomStateBox.get(key);
737 : if (raw == null) continue;
738 9 : unimportantEvents.add(Event.fromJson(copyMap(raw), room));
739 : }
740 :
741 18 : return unimportantEvents.where((event) => event.stateKey != null).toList();
742 : }
743 :
744 32 : @override
745 : Future<User?> getUser(String userId, Room room) async {
746 : final state =
747 160 : await _roomMembersBox.get(TupleKey(room.id, userId).toString());
748 : if (state == null) return null;
749 93 : return Event.fromJson(copyMap(state), room).asUser;
750 : }
751 :
752 32 : @override
753 : Future<Map<String, DeviceKeysList>> getUserDeviceKeys(Client client) =>
754 32 : runBenchmarked<Map<String, DeviceKeysList>>(
755 32 : 'Get all user device keys from store', () async {
756 : final deviceKeysOutdated =
757 64 : await _userDeviceKeysOutdatedBox.getAllValues();
758 32 : if (deviceKeysOutdated.isEmpty) {
759 32 : return {};
760 : }
761 1 : final res = <String, DeviceKeysList>{};
762 2 : final userDeviceKeys = await _userDeviceKeysBox.getAllValues();
763 : final userCrossSigningKeys =
764 2 : await _userCrossSigningKeysBox.getAllValues();
765 2 : for (final userId in deviceKeysOutdated.keys) {
766 3 : final deviceKeysBoxKeys = userDeviceKeys.keys.where((tuple) {
767 1 : final tupleKey = TupleKey.fromString(tuple);
768 3 : return tupleKey.parts.first == userId;
769 : });
770 : final crossSigningKeysBoxKeys =
771 3 : userCrossSigningKeys.keys.where((tuple) {
772 1 : final tupleKey = TupleKey.fromString(tuple);
773 3 : return tupleKey.parts.first == userId;
774 : });
775 1 : final childEntries = deviceKeysBoxKeys.map(
776 1 : (key) {
777 1 : final userDeviceKey = userDeviceKeys[key];
778 : if (userDeviceKey == null) return null;
779 1 : return copyMap(userDeviceKey);
780 : },
781 : );
782 1 : final crossSigningEntries = crossSigningKeysBoxKeys.map(
783 1 : (key) {
784 1 : final crossSigningKey = userCrossSigningKeys[key];
785 : if (crossSigningKey == null) return null;
786 1 : return copyMap(crossSigningKey);
787 : },
788 : );
789 2 : res[userId] = DeviceKeysList.fromDbJson(
790 1 : {
791 1 : 'client_id': client.id,
792 : 'user_id': userId,
793 1 : 'outdated': deviceKeysOutdated[userId],
794 : },
795 : childEntries
796 2 : .where((c) => c != null)
797 1 : .toList()
798 1 : .cast<Map<String, dynamic>>(),
799 : crossSigningEntries
800 2 : .where((c) => c != null)
801 1 : .toList()
802 1 : .cast<Map<String, dynamic>>(),
803 : client,
804 : );
805 : }
806 : return res;
807 : });
808 :
809 32 : @override
810 : Future<List<User>> getUsers(Room room) async {
811 32 : final users = <User>[];
812 64 : final keys = (await _roomMembersBox.getAllKeys())
813 218 : .where((key) => TupleKey.fromString(key).parts.first == room.id)
814 32 : .toList();
815 64 : final states = await _roomMembersBox.getAll(keys);
816 35 : states.removeWhere((state) => state == null);
817 35 : for (final state in states) {
818 12 : users.add(Event.fromJson(copyMap(state!), room).asUser);
819 : }
820 :
821 : return users;
822 : }
823 :
824 34 : @override
825 : Future<int> insertClient(
826 : String name,
827 : String homeserverUrl,
828 : String token,
829 : DateTime? tokenExpiresAt,
830 : String? refreshToken,
831 : String userId,
832 : String? deviceId,
833 : String? deviceName,
834 : String? prevBatch,
835 : String? olmAccount,
836 : ) async {
837 68 : await transaction(() async {
838 68 : await _clientBox.put('homeserver_url', homeserverUrl);
839 68 : await _clientBox.put('token', token);
840 : if (tokenExpiresAt == null) {
841 66 : await _clientBox.delete('token_expires_at');
842 : } else {
843 2 : await _clientBox.put(
844 : 'token_expires_at',
845 2 : tokenExpiresAt.millisecondsSinceEpoch.toString(),
846 : );
847 : }
848 : if (refreshToken == null) {
849 12 : await _clientBox.delete('refresh_token');
850 : } else {
851 64 : await _clientBox.put('refresh_token', refreshToken);
852 : }
853 68 : await _clientBox.put('user_id', userId);
854 : if (deviceId == null) {
855 4 : await _clientBox.delete('device_id');
856 : } else {
857 64 : await _clientBox.put('device_id', deviceId);
858 : }
859 : if (deviceName == null) {
860 4 : await _clientBox.delete('device_name');
861 : } else {
862 64 : await _clientBox.put('device_name', deviceName);
863 : }
864 : if (prevBatch == null) {
865 66 : await _clientBox.delete('prev_batch');
866 : } else {
867 4 : await _clientBox.put('prev_batch', prevBatch);
868 : }
869 : if (olmAccount == null) {
870 20 : await _clientBox.delete('olm_account');
871 : } else {
872 48 : await _clientBox.put('olm_account', olmAccount);
873 : }
874 68 : await _clientBox.delete('sync_filter_id');
875 : });
876 : return 0;
877 : }
878 :
879 2 : @override
880 : Future<int> insertIntoToDeviceQueue(
881 : String type,
882 : String txnId,
883 : String content,
884 : ) async {
885 4 : final id = DateTime.now().millisecondsSinceEpoch;
886 8 : await _toDeviceQueueBox.put(id.toString(), {
887 : 'type': type,
888 : 'txn_id': txnId,
889 : 'content': content,
890 : });
891 : return id;
892 : }
893 :
894 5 : @override
895 : Future<void> markInboundGroupSessionAsUploaded(
896 : String roomId,
897 : String sessionId,
898 : ) async {
899 10 : await _inboundGroupSessionsUploadQueueBox.delete(sessionId);
900 : return;
901 : }
902 :
903 2 : @override
904 : Future<void> markInboundGroupSessionsAsNeedingUpload() async {
905 4 : final keys = await _inboundGroupSessionsBox.getAllKeys();
906 4 : for (final sessionId in keys) {
907 2 : final raw = copyMap(
908 4 : await _inboundGroupSessionsBox.get(sessionId) ?? {},
909 : );
910 2 : if (raw.isEmpty) continue;
911 2 : final roomId = raw.tryGet<String>('room_id');
912 : if (roomId == null) continue;
913 4 : await _inboundGroupSessionsUploadQueueBox.put(sessionId, roomId);
914 : }
915 : return;
916 : }
917 :
918 10 : @override
919 : Future<void> removeEvent(String eventId, String roomId) async {
920 40 : await _eventsBox.delete(TupleKey(roomId, eventId).toString());
921 20 : final keys = await _timelineFragmentsBox.getAllKeys();
922 20 : for (final key in keys) {
923 10 : final multiKey = TupleKey.fromString(key);
924 30 : if (multiKey.parts.first != roomId) continue;
925 : final eventIds =
926 30 : List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
927 10 : final prevLength = eventIds.length;
928 30 : eventIds.removeWhere((id) => id == eventId);
929 20 : if (eventIds.length < prevLength) {
930 20 : await _timelineFragmentsBox.put(key, eventIds);
931 : }
932 : }
933 : return;
934 : }
935 :
936 2 : @override
937 : Future<void> removeOutboundGroupSession(String roomId) async {
938 4 : await _outboundGroupSessionsBox.delete(roomId);
939 : return;
940 : }
941 :
942 4 : @override
943 : Future<void> removeUserCrossSigningKey(
944 : String userId,
945 : String publicKey,
946 : ) async {
947 4 : await _userCrossSigningKeysBox
948 12 : .delete(TupleKey(userId, publicKey).toString());
949 : return;
950 : }
951 :
952 1 : @override
953 : Future<void> removeUserDeviceKey(String userId, String deviceId) async {
954 4 : await _userDeviceKeysBox.delete(TupleKey(userId, deviceId).toString());
955 : return;
956 : }
957 :
958 3 : @override
959 : Future<void> setBlockedUserCrossSigningKey(
960 : bool blocked,
961 : String userId,
962 : String publicKey,
963 : ) async {
964 3 : final raw = copyMap(
965 3 : await _userCrossSigningKeysBox
966 9 : .get(TupleKey(userId, publicKey).toString()) ??
967 0 : {},
968 : );
969 3 : raw['blocked'] = blocked;
970 6 : await _userCrossSigningKeysBox.put(
971 6 : TupleKey(userId, publicKey).toString(),
972 : raw,
973 : );
974 : return;
975 : }
976 :
977 3 : @override
978 : Future<void> setBlockedUserDeviceKey(
979 : bool blocked,
980 : String userId,
981 : String deviceId,
982 : ) async {
983 3 : final raw = copyMap(
984 12 : await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
985 : );
986 3 : raw['blocked'] = blocked;
987 6 : await _userDeviceKeysBox.put(
988 6 : TupleKey(userId, deviceId).toString(),
989 : raw,
990 : );
991 : return;
992 : }
993 :
994 1 : @override
995 : Future<void> setLastActiveUserDeviceKey(
996 : int lastActive,
997 : String userId,
998 : String deviceId,
999 : ) async {
1000 1 : final raw = copyMap(
1001 4 : await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
1002 : );
1003 :
1004 1 : raw['last_active'] = lastActive;
1005 2 : await _userDeviceKeysBox.put(
1006 2 : TupleKey(userId, deviceId).toString(),
1007 : raw,
1008 : );
1009 : }
1010 :
1011 7 : @override
1012 : Future<void> setLastSentMessageUserDeviceKey(
1013 : String lastSentMessage,
1014 : String userId,
1015 : String deviceId,
1016 : ) async {
1017 7 : final raw = copyMap(
1018 28 : await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
1019 : );
1020 7 : raw['last_sent_message'] = lastSentMessage;
1021 14 : await _userDeviceKeysBox.put(
1022 14 : TupleKey(userId, deviceId).toString(),
1023 : raw,
1024 : );
1025 : }
1026 :
1027 2 : @override
1028 : Future<void> setRoomPrevBatch(
1029 : String? prevBatch,
1030 : String roomId,
1031 : Client client,
1032 : ) async {
1033 4 : final raw = await _roomsBox.get(roomId);
1034 : if (raw == null) return;
1035 2 : final room = Room.fromJson(copyMap(raw), client);
1036 1 : room.prev_batch = prevBatch;
1037 3 : await _roomsBox.put(roomId, room.toJson());
1038 : return;
1039 : }
1040 :
1041 6 : @override
1042 : Future<void> setVerifiedUserCrossSigningKey(
1043 : bool verified,
1044 : String userId,
1045 : String publicKey,
1046 : ) async {
1047 6 : final raw = copyMap(
1048 6 : (await _userCrossSigningKeysBox
1049 18 : .get(TupleKey(userId, publicKey).toString())) ??
1050 1 : {},
1051 : );
1052 6 : raw['verified'] = verified;
1053 12 : await _userCrossSigningKeysBox.put(
1054 12 : TupleKey(userId, publicKey).toString(),
1055 : raw,
1056 : );
1057 : return;
1058 : }
1059 :
1060 4 : @override
1061 : Future<void> setVerifiedUserDeviceKey(
1062 : bool verified,
1063 : String userId,
1064 : String deviceId,
1065 : ) async {
1066 4 : final raw = copyMap(
1067 16 : await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
1068 : );
1069 4 : raw['verified'] = verified;
1070 8 : await _userDeviceKeysBox.put(
1071 8 : TupleKey(userId, deviceId).toString(),
1072 : raw,
1073 : );
1074 : return;
1075 : }
1076 :
1077 32 : @override
1078 : Future<void> storeAccountData(
1079 : String type,
1080 : Map<String, Object?> content,
1081 : ) async {
1082 64 : await _accountDataBox.put(type, content);
1083 : return;
1084 : }
1085 :
1086 32 : @override
1087 : Future<void> storeRoomAccountData(String roomId, BasicEvent event) async {
1088 64 : await _roomAccountDataBox.put(
1089 96 : TupleKey(roomId, event.type).toString(),
1090 32 : event.toJson(),
1091 : );
1092 : return;
1093 : }
1094 :
1095 34 : @override
1096 : Future<void> storeEventUpdate(
1097 : String roomId,
1098 : StrippedStateEvent event,
1099 : EventUpdateType type,
1100 : Client client,
1101 : ) async {
1102 : final tmpRoom =
1103 40 : client.getRoomById(roomId) ?? Room(id: roomId, client: client);
1104 :
1105 : // In case of this is a redaction event
1106 70 : if (event.type == EventTypes.Redaction && event is MatrixEvent) {
1107 2 : final redactionEvent = Event.fromMatrixEvent(event, tmpRoom);
1108 2 : final eventId = redactionEvent.redacts;
1109 : final redactedEvent =
1110 2 : eventId != null ? await getEventById(eventId, tmpRoom) : null;
1111 : if (redactedEvent != null) {
1112 0 : redactedEvent.setRedactionEvent(redactionEvent);
1113 0 : await _eventsBox.put(
1114 0 : TupleKey(roomId, redactedEvent.eventId).toString(),
1115 0 : redactedEvent.toJson(),
1116 : );
1117 : }
1118 : }
1119 :
1120 : // Store a common message event
1121 102 : if ({EventUpdateType.timeline, EventUpdateType.history}.contains(type) &&
1122 34 : event is MatrixEvent) {
1123 34 : final timelineEvent = Event.fromMatrixEvent(event, tmpRoom);
1124 : // Is this ID already in the store?
1125 : final prevEvent =
1126 170 : await _eventsBox.get(TupleKey(roomId, event.eventId).toString());
1127 : final prevStatus = prevEvent == null
1128 : ? null
1129 9 : : () {
1130 9 : final json = copyMap(prevEvent);
1131 9 : final statusInt = json.tryGet<int>('status') ??
1132 : json
1133 0 : .tryGetMap<String, dynamic>('unsigned')
1134 0 : ?.tryGet<int>(messageSendingStatusKey);
1135 9 : return statusInt == null ? null : eventStatusFromInt(statusInt);
1136 9 : }();
1137 :
1138 : // calculate the status
1139 34 : final newStatus = timelineEvent.status;
1140 :
1141 : // Is this the response to a sending event which is already synced? Then
1142 : // there is nothing to do here.
1143 40 : if (!newStatus.isSynced && prevStatus != null && prevStatus.isSynced) {
1144 : return;
1145 : }
1146 :
1147 34 : final status = newStatus.isError || prevStatus == null
1148 : ? newStatus
1149 7 : : latestEventStatus(
1150 : prevStatus,
1151 : newStatus,
1152 : );
1153 :
1154 34 : timelineEvent.status = status;
1155 :
1156 34 : final eventId = timelineEvent.eventId;
1157 : // In case this event has sent from this account we have a transaction ID
1158 34 : final transactionId = timelineEvent.transactionId;
1159 68 : await _eventsBox.put(
1160 68 : TupleKey(roomId, eventId).toString(),
1161 34 : timelineEvent.toJson(),
1162 : );
1163 :
1164 : // Update timeline fragments
1165 102 : final key = TupleKey(roomId, status.isSent ? '' : 'SENDING').toString();
1166 :
1167 : final eventIds =
1168 136 : List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
1169 :
1170 34 : if (!eventIds.contains(eventId)) {
1171 34 : if (type == EventUpdateType.history) {
1172 2 : eventIds.add(eventId);
1173 : } else {
1174 34 : eventIds.insert(0, eventId);
1175 : }
1176 68 : await _timelineFragmentsBox.put(key, eventIds);
1177 7 : } else if (status.isSynced &&
1178 : prevStatus != null &&
1179 5 : prevStatus.isSent &&
1180 5 : type != EventUpdateType.history) {
1181 : // Status changes from 1 -> 2? Make sure event is correctly sorted.
1182 5 : eventIds.remove(eventId);
1183 5 : eventIds.insert(0, eventId);
1184 : }
1185 :
1186 : // If event comes from server timeline, remove sending events with this ID
1187 34 : if (status.isSent) {
1188 68 : final key = TupleKey(roomId, 'SENDING').toString();
1189 : final eventIds =
1190 136 : List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
1191 52 : final i = eventIds.indexWhere((id) => id == eventId);
1192 68 : if (i != -1) {
1193 6 : await _timelineFragmentsBox.put(key, eventIds..removeAt(i));
1194 : }
1195 : }
1196 :
1197 : // Is there a transaction id? Then delete the event with this id.
1198 68 : if (!status.isError && !status.isSending && transactionId != null) {
1199 9 : await removeEvent(transactionId, roomId);
1200 : }
1201 : }
1202 :
1203 34 : final stateKey = event.stateKey;
1204 : // Store a common state event
1205 : if (stateKey != null &&
1206 : // Don't store events as state updates when paginating backwards.
1207 : {
1208 32 : EventUpdateType.timeline,
1209 32 : EventUpdateType.state,
1210 32 : EventUpdateType.inviteState,
1211 32 : }.contains(type)) {
1212 64 : if (event.type == EventTypes.RoomMember) {
1213 62 : await _roomMembersBox.put(
1214 31 : TupleKey(
1215 : roomId,
1216 : stateKey,
1217 31 : ).toString(),
1218 31 : event.toJson(),
1219 : );
1220 : } else {
1221 96 : final roomStateBox = client.importantStateEvents.contains(event.type)
1222 32 : ? _preloadRoomStateBox
1223 31 : : _nonPreloadRoomStateBox;
1224 32 : final key = TupleKey(
1225 : roomId,
1226 32 : event.type,
1227 : stateKey,
1228 32 : ).toString();
1229 :
1230 64 : await roomStateBox.put(key, event.toJson());
1231 : }
1232 : }
1233 : }
1234 :
1235 24 : @override
1236 : Future<void> storeInboundGroupSession(
1237 : String roomId,
1238 : String sessionId,
1239 : String pickle,
1240 : String content,
1241 : String indexes,
1242 : String allowedAtIndex,
1243 : String senderKey,
1244 : String senderClaimedKey,
1245 : ) async {
1246 24 : final json = StoredInboundGroupSession(
1247 : roomId: roomId,
1248 : sessionId: sessionId,
1249 : pickle: pickle,
1250 : content: content,
1251 : indexes: indexes,
1252 : allowedAtIndex: allowedAtIndex,
1253 : senderKey: senderKey,
1254 : senderClaimedKeys: senderClaimedKey,
1255 24 : ).toJson();
1256 48 : await _inboundGroupSessionsBox.put(
1257 : sessionId,
1258 : json,
1259 : );
1260 : // Mark this session as needing upload too
1261 48 : await _inboundGroupSessionsUploadQueueBox.put(sessionId, roomId);
1262 : return;
1263 : }
1264 :
1265 6 : @override
1266 : Future<void> storeOutboundGroupSession(
1267 : String roomId,
1268 : String pickle,
1269 : String deviceIds,
1270 : int creationTime,
1271 : ) async {
1272 18 : await _outboundGroupSessionsBox.put(roomId, <String, dynamic>{
1273 : 'room_id': roomId,
1274 : 'pickle': pickle,
1275 : 'device_ids': deviceIds,
1276 : 'creation_time': creationTime,
1277 : });
1278 : return;
1279 : }
1280 :
1281 31 : @override
1282 : Future<void> storePrevBatch(
1283 : String prevBatch,
1284 : ) async {
1285 93 : if ((await _clientBox.getAllKeys()).isEmpty) return;
1286 62 : await _clientBox.put('prev_batch', prevBatch);
1287 : return;
1288 : }
1289 :
1290 32 : @override
1291 : Future<void> storeRoomUpdate(
1292 : String roomId,
1293 : SyncRoomUpdate roomUpdate,
1294 : Event? lastEvent,
1295 : Client client,
1296 : ) async {
1297 : // Leave room if membership is leave
1298 32 : if (roomUpdate is LeftRoomUpdate) {
1299 31 : await forgetRoom(roomId);
1300 : return;
1301 : }
1302 32 : final membership = roomUpdate is LeftRoomUpdate
1303 : ? Membership.leave
1304 32 : : roomUpdate is InvitedRoomUpdate
1305 : ? Membership.invite
1306 : : Membership.join;
1307 : // Make sure room exists
1308 64 : final currentRawRoom = await _roomsBox.get(roomId);
1309 : if (currentRawRoom == null) {
1310 64 : await _roomsBox.put(
1311 : roomId,
1312 32 : roomUpdate is JoinedRoomUpdate
1313 32 : ? Room(
1314 : client: client,
1315 : id: roomId,
1316 : membership: membership,
1317 : highlightCount:
1318 94 : roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
1319 : 0,
1320 : notificationCount: roomUpdate
1321 63 : .unreadNotifications?.notificationCount
1322 31 : ?.toInt() ??
1323 : 0,
1324 63 : prev_batch: roomUpdate.timeline?.prevBatch,
1325 32 : summary: roomUpdate.summary,
1326 : lastEvent: lastEvent,
1327 32 : ).toJson()
1328 31 : : Room(
1329 : client: client,
1330 : id: roomId,
1331 : membership: membership,
1332 : lastEvent: lastEvent,
1333 31 : ).toJson(),
1334 : );
1335 11 : } else if (roomUpdate is JoinedRoomUpdate) {
1336 22 : final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
1337 22 : await _roomsBox.put(
1338 : roomId,
1339 11 : Room(
1340 : client: client,
1341 : id: roomId,
1342 : membership: membership,
1343 : highlightCount:
1344 13 : roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
1345 11 : currentRoom.highlightCount,
1346 : notificationCount:
1347 13 : roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
1348 11 : currentRoom.notificationCount,
1349 32 : prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
1350 11 : summary: RoomSummary.fromJson(
1351 22 : currentRoom.summary.toJson()
1352 34 : ..addAll(roomUpdate.summary?.toJson() ?? {}),
1353 : ),
1354 : lastEvent: lastEvent,
1355 11 : ).toJson(),
1356 : );
1357 : }
1358 : }
1359 :
1360 31 : @override
1361 : Future<void> deleteTimelineForRoom(String roomId) =>
1362 124 : _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
1363 :
1364 8 : @override
1365 : Future<void> storeSSSSCache(
1366 : String type,
1367 : String keyId,
1368 : String ciphertext,
1369 : String content,
1370 : ) async {
1371 16 : await _ssssCacheBox.put(
1372 : type,
1373 8 : SSSSCache(
1374 : type: type,
1375 : keyId: keyId,
1376 : ciphertext: ciphertext,
1377 : content: content,
1378 8 : ).toJson(),
1379 : );
1380 : }
1381 :
1382 32 : @override
1383 : Future<void> storeSyncFilterId(
1384 : String syncFilterId,
1385 : ) async {
1386 64 : await _clientBox.put('sync_filter_id', syncFilterId);
1387 : }
1388 :
1389 32 : @override
1390 : Future<void> storeUserCrossSigningKey(
1391 : String userId,
1392 : String publicKey,
1393 : String content,
1394 : bool verified,
1395 : bool blocked,
1396 : ) async {
1397 64 : await _userCrossSigningKeysBox.put(
1398 64 : TupleKey(userId, publicKey).toString(),
1399 32 : {
1400 : 'user_id': userId,
1401 : 'public_key': publicKey,
1402 : 'content': content,
1403 : 'verified': verified,
1404 : 'blocked': blocked,
1405 : },
1406 : );
1407 : }
1408 :
1409 32 : @override
1410 : Future<void> storeUserDeviceKey(
1411 : String userId,
1412 : String deviceId,
1413 : String content,
1414 : bool verified,
1415 : bool blocked,
1416 : int lastActive,
1417 : ) async {
1418 160 : await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
1419 : 'user_id': userId,
1420 : 'device_id': deviceId,
1421 : 'content': content,
1422 : 'verified': verified,
1423 : 'blocked': blocked,
1424 : 'last_active': lastActive,
1425 : 'last_sent_message': '',
1426 : });
1427 : return;
1428 : }
1429 :
1430 32 : @override
1431 : Future<void> storeUserDeviceKeysInfo(String userId, bool outdated) async {
1432 64 : await _userDeviceKeysOutdatedBox.put(userId, outdated);
1433 : return;
1434 : }
1435 :
1436 34 : @override
1437 : Future<void> transaction(Future<void> Function() action) =>
1438 68 : _collection.transaction(action);
1439 :
1440 2 : @override
1441 : Future<void> updateClient(
1442 : String homeserverUrl,
1443 : String token,
1444 : DateTime? tokenExpiresAt,
1445 : String? refreshToken,
1446 : String userId,
1447 : String? deviceId,
1448 : String? deviceName,
1449 : String? prevBatch,
1450 : String? olmAccount,
1451 : ) async {
1452 4 : await transaction(() async {
1453 4 : await _clientBox.put('homeserver_url', homeserverUrl);
1454 4 : await _clientBox.put('token', token);
1455 : if (tokenExpiresAt == null) {
1456 0 : await _clientBox.delete('token_expires_at');
1457 : } else {
1458 4 : await _clientBox.put(
1459 : 'token_expires_at',
1460 4 : tokenExpiresAt.millisecondsSinceEpoch.toString(),
1461 : );
1462 : }
1463 : if (refreshToken == null) {
1464 0 : await _clientBox.delete('refresh_token');
1465 : } else {
1466 4 : await _clientBox.put('refresh_token', refreshToken);
1467 : }
1468 4 : await _clientBox.put('user_id', userId);
1469 : if (deviceId == null) {
1470 0 : await _clientBox.delete('device_id');
1471 : } else {
1472 4 : await _clientBox.put('device_id', deviceId);
1473 : }
1474 : if (deviceName == null) {
1475 0 : await _clientBox.delete('device_name');
1476 : } else {
1477 4 : await _clientBox.put('device_name', deviceName);
1478 : }
1479 : if (prevBatch == null) {
1480 0 : await _clientBox.delete('prev_batch');
1481 : } else {
1482 4 : await _clientBox.put('prev_batch', prevBatch);
1483 : }
1484 : if (olmAccount == null) {
1485 0 : await _clientBox.delete('olm_account');
1486 : } else {
1487 4 : await _clientBox.put('olm_account', olmAccount);
1488 : }
1489 : });
1490 : return;
1491 : }
1492 :
1493 24 : @override
1494 : Future<void> updateClientKeys(
1495 : String olmAccount,
1496 : ) async {
1497 48 : await _clientBox.put('olm_account', olmAccount);
1498 : return;
1499 : }
1500 :
1501 2 : @override
1502 : Future<void> updateInboundGroupSessionAllowedAtIndex(
1503 : String allowedAtIndex,
1504 : String roomId,
1505 : String sessionId,
1506 : ) async {
1507 4 : final raw = await _inboundGroupSessionsBox.get(sessionId);
1508 : if (raw == null) {
1509 0 : Logs().w(
1510 : 'Tried to update inbound group session as uploaded which wasnt found in the database!',
1511 : );
1512 : return;
1513 : }
1514 2 : raw['allowed_at_index'] = allowedAtIndex;
1515 4 : await _inboundGroupSessionsBox.put(sessionId, raw);
1516 : return;
1517 : }
1518 :
1519 4 : @override
1520 : Future<void> updateInboundGroupSessionIndexes(
1521 : String indexes,
1522 : String roomId,
1523 : String sessionId,
1524 : ) async {
1525 8 : final raw = await _inboundGroupSessionsBox.get(sessionId);
1526 : if (raw == null) {
1527 0 : Logs().w(
1528 : 'Tried to update inbound group session indexes of a session which was not found in the database!',
1529 : );
1530 : return;
1531 : }
1532 4 : final json = copyMap(raw);
1533 4 : json['indexes'] = indexes;
1534 8 : await _inboundGroupSessionsBox.put(sessionId, json);
1535 : return;
1536 : }
1537 :
1538 2 : @override
1539 : Future<List<StoredInboundGroupSession>> getAllInboundGroupSessions() async {
1540 4 : final rawSessions = await _inboundGroupSessionsBox.getAllValues();
1541 2 : return rawSessions.values
1542 5 : .map((raw) => StoredInboundGroupSession.fromJson(copyMap(raw)))
1543 2 : .toList();
1544 : }
1545 :
1546 31 : @override
1547 : Future<void> addSeenDeviceId(
1548 : String userId,
1549 : String deviceId,
1550 : String publicKeys,
1551 : ) =>
1552 124 : _seenDeviceIdsBox.put(TupleKey(userId, deviceId).toString(), publicKeys);
1553 :
1554 31 : @override
1555 : Future<void> addSeenPublicKey(
1556 : String publicKey,
1557 : String deviceId,
1558 : ) =>
1559 62 : _seenDeviceKeysBox.put(publicKey, deviceId);
1560 :
1561 31 : @override
1562 : Future<String?> deviceIdSeen(userId, deviceId) async {
1563 : final raw =
1564 124 : await _seenDeviceIdsBox.get(TupleKey(userId, deviceId).toString());
1565 : if (raw == null) return null;
1566 : return raw;
1567 : }
1568 :
1569 31 : @override
1570 : Future<String?> publicKeySeen(String publicKey) async {
1571 62 : final raw = await _seenDeviceKeysBox.get(publicKey);
1572 : if (raw == null) return null;
1573 : return raw;
1574 : }
1575 :
1576 0 : @override
1577 : Future<String> exportDump() async {
1578 0 : final dataMap = {
1579 0 : _clientBoxName: await _clientBox.getAllValues(),
1580 0 : _accountDataBoxName: await _accountDataBox.getAllValues(),
1581 0 : _roomsBoxName: await _roomsBox.getAllValues(),
1582 0 : _preloadRoomStateBoxName: await _preloadRoomStateBox.getAllValues(),
1583 0 : _nonPreloadRoomStateBoxName: await _nonPreloadRoomStateBox.getAllValues(),
1584 0 : _roomMembersBoxName: await _roomMembersBox.getAllValues(),
1585 0 : _toDeviceQueueBoxName: await _toDeviceQueueBox.getAllValues(),
1586 0 : _roomAccountDataBoxName: await _roomAccountDataBox.getAllValues(),
1587 : _inboundGroupSessionsBoxName:
1588 0 : await _inboundGroupSessionsBox.getAllValues(),
1589 : _inboundGroupSessionsUploadQueueBoxName:
1590 0 : await _inboundGroupSessionsUploadQueueBox.getAllValues(),
1591 : _outboundGroupSessionsBoxName:
1592 0 : await _outboundGroupSessionsBox.getAllValues(),
1593 0 : _olmSessionsBoxName: await _olmSessionsBox.getAllValues(),
1594 0 : _userDeviceKeysBoxName: await _userDeviceKeysBox.getAllValues(),
1595 : _userDeviceKeysOutdatedBoxName:
1596 0 : await _userDeviceKeysOutdatedBox.getAllValues(),
1597 : _userCrossSigningKeysBoxName:
1598 0 : await _userCrossSigningKeysBox.getAllValues(),
1599 0 : _ssssCacheBoxName: await _ssssCacheBox.getAllValues(),
1600 0 : _presencesBoxName: await _presencesBox.getAllValues(),
1601 0 : _timelineFragmentsBoxName: await _timelineFragmentsBox.getAllValues(),
1602 0 : _eventsBoxName: await _eventsBox.getAllValues(),
1603 0 : _seenDeviceIdsBoxName: await _seenDeviceIdsBox.getAllValues(),
1604 0 : _seenDeviceKeysBoxName: await _seenDeviceKeysBox.getAllValues(),
1605 : };
1606 0 : final json = jsonEncode(dataMap);
1607 0 : await clear();
1608 : return json;
1609 : }
1610 :
1611 0 : @override
1612 : Future<bool> importDump(String export) async {
1613 : try {
1614 0 : await clear();
1615 0 : await open();
1616 0 : final json = Map.from(jsonDecode(export)).cast<String, Map>();
1617 0 : for (final key in json[_clientBoxName]!.keys) {
1618 0 : await _clientBox.put(key, json[_clientBoxName]![key]);
1619 : }
1620 0 : for (final key in json[_accountDataBoxName]!.keys) {
1621 0 : await _accountDataBox.put(key, json[_accountDataBoxName]![key]);
1622 : }
1623 0 : for (final key in json[_roomsBoxName]!.keys) {
1624 0 : await _roomsBox.put(key, json[_roomsBoxName]![key]);
1625 : }
1626 0 : for (final key in json[_preloadRoomStateBoxName]!.keys) {
1627 0 : await _preloadRoomStateBox.put(
1628 : key,
1629 0 : json[_preloadRoomStateBoxName]![key],
1630 : );
1631 : }
1632 0 : for (final key in json[_nonPreloadRoomStateBoxName]!.keys) {
1633 0 : await _nonPreloadRoomStateBox.put(
1634 : key,
1635 0 : json[_nonPreloadRoomStateBoxName]![key],
1636 : );
1637 : }
1638 0 : for (final key in json[_roomMembersBoxName]!.keys) {
1639 0 : await _roomMembersBox.put(key, json[_roomMembersBoxName]![key]);
1640 : }
1641 0 : for (final key in json[_toDeviceQueueBoxName]!.keys) {
1642 0 : await _toDeviceQueueBox.put(key, json[_toDeviceQueueBoxName]![key]);
1643 : }
1644 0 : for (final key in json[_roomAccountDataBoxName]!.keys) {
1645 0 : await _roomAccountDataBox.put(key, json[_roomAccountDataBoxName]![key]);
1646 : }
1647 0 : for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
1648 0 : await _inboundGroupSessionsBox.put(
1649 : key,
1650 0 : json[_inboundGroupSessionsBoxName]![key],
1651 : );
1652 : }
1653 0 : for (final key in json[_inboundGroupSessionsUploadQueueBoxName]!.keys) {
1654 0 : await _inboundGroupSessionsUploadQueueBox.put(
1655 : key,
1656 0 : json[_inboundGroupSessionsUploadQueueBoxName]![key],
1657 : );
1658 : }
1659 0 : for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
1660 0 : await _outboundGroupSessionsBox.put(
1661 : key,
1662 0 : json[_outboundGroupSessionsBoxName]![key],
1663 : );
1664 : }
1665 0 : for (final key in json[_olmSessionsBoxName]!.keys) {
1666 0 : await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
1667 : }
1668 0 : for (final key in json[_userDeviceKeysBoxName]!.keys) {
1669 0 : await _userDeviceKeysBox.put(key, json[_userDeviceKeysBoxName]![key]);
1670 : }
1671 0 : for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
1672 0 : await _userDeviceKeysOutdatedBox.put(
1673 : key,
1674 0 : json[_userDeviceKeysOutdatedBoxName]![key],
1675 : );
1676 : }
1677 0 : for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
1678 0 : await _userCrossSigningKeysBox.put(
1679 : key,
1680 0 : json[_userCrossSigningKeysBoxName]![key],
1681 : );
1682 : }
1683 0 : for (final key in json[_ssssCacheBoxName]!.keys) {
1684 0 : await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
1685 : }
1686 0 : for (final key in json[_presencesBoxName]!.keys) {
1687 0 : await _presencesBox.put(key, json[_presencesBoxName]![key]);
1688 : }
1689 0 : for (final key in json[_timelineFragmentsBoxName]!.keys) {
1690 0 : await _timelineFragmentsBox.put(
1691 : key,
1692 0 : json[_timelineFragmentsBoxName]![key],
1693 : );
1694 : }
1695 0 : for (final key in json[_seenDeviceIdsBoxName]!.keys) {
1696 0 : await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]);
1697 : }
1698 0 : for (final key in json[_seenDeviceKeysBoxName]!.keys) {
1699 0 : await _seenDeviceKeysBox.put(key, json[_seenDeviceKeysBoxName]![key]);
1700 : }
1701 : return true;
1702 : } catch (e, s) {
1703 0 : Logs().e('Database import error: ', e, s);
1704 : return false;
1705 : }
1706 : }
1707 :
1708 1 : @override
1709 : Future<List<String>> getEventIdList(
1710 : Room room, {
1711 : int start = 0,
1712 : bool includeSending = false,
1713 : int? limit,
1714 : }) =>
1715 2 : runBenchmarked<List<String>>('Get event id list', () async {
1716 : // Get the synced event IDs from the store
1717 3 : final timelineKey = TupleKey(room.id, '').toString();
1718 1 : final timelineEventIds = List<String>.from(
1719 2 : (await _timelineFragmentsBox.get(timelineKey)) ?? [],
1720 : );
1721 :
1722 : // Get the local stored SENDING events from the store
1723 : late final List<String> sendingEventIds;
1724 : if (!includeSending) {
1725 1 : sendingEventIds = [];
1726 : } else {
1727 0 : final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
1728 0 : sendingEventIds = List<String>.from(
1729 0 : (await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
1730 : );
1731 : }
1732 :
1733 : // Combine those two lists while respecting the start and limit parameters.
1734 : // Create a new list object instead of concatonating list to prevent
1735 : // random type errors.
1736 1 : final eventIds = [
1737 : ...sendingEventIds,
1738 1 : ...timelineEventIds,
1739 : ];
1740 0 : if (limit != null && eventIds.length > limit) {
1741 0 : eventIds.removeRange(limit, eventIds.length);
1742 : }
1743 :
1744 : return eventIds;
1745 : });
1746 :
1747 32 : @override
1748 : Future<void> storePresence(String userId, CachedPresence presence) =>
1749 96 : _presencesBox.put(userId, presence.toJson());
1750 :
1751 1 : @override
1752 : Future<CachedPresence?> getPresence(String userId) async {
1753 2 : final rawPresence = await _presencesBox.get(userId);
1754 : if (rawPresence == null) return null;
1755 :
1756 2 : return CachedPresence.fromJson(copyMap(rawPresence));
1757 : }
1758 :
1759 1 : @override
1760 : Future<void> storeWellKnown(DiscoveryInformation? discoveryInformation) {
1761 : if (discoveryInformation == null) {
1762 0 : return _clientBox.delete('discovery_information');
1763 : }
1764 2 : return _clientBox.put(
1765 : 'discovery_information',
1766 2 : jsonEncode(discoveryInformation.toJson()),
1767 : );
1768 : }
1769 :
1770 31 : @override
1771 : Future<DiscoveryInformation?> getWellKnown() async {
1772 : final rawDiscoveryInformation =
1773 62 : await _clientBox.get('discovery_information');
1774 : if (rawDiscoveryInformation == null) return null;
1775 2 : return DiscoveryInformation.fromJson(jsonDecode(rawDiscoveryInformation));
1776 : }
1777 :
1778 9 : @override
1779 : Future<void> delete() async {
1780 : // database?.path is null on web
1781 18 : await _collection.deleteDatabase(
1782 18 : database?.path ?? name,
1783 9 : sqfliteFactory ?? idbFactory,
1784 : );
1785 : }
1786 :
1787 32 : @override
1788 : Future<void> markUserProfileAsOutdated(userId) async {
1789 32 : final profile = await getUserProfile(userId);
1790 : if (profile == null) return;
1791 4 : await _userProfilesBox.put(
1792 : userId,
1793 2 : CachedProfileInformation.fromProfile(
1794 : profile as ProfileInformation,
1795 : outdated: true,
1796 2 : updated: profile.updated,
1797 2 : ).toJson(),
1798 : );
1799 : }
1800 :
1801 32 : @override
1802 : Future<CachedProfileInformation?> getUserProfile(String userId) =>
1803 96 : _userProfilesBox.get(userId).then(
1804 32 : (json) => json == null
1805 : ? null
1806 4 : : CachedProfileInformation.fromJson(copyMap(json)),
1807 : );
1808 :
1809 4 : @override
1810 : Future<void> storeUserProfile(
1811 : String userId,
1812 : CachedProfileInformation profile,
1813 : ) =>
1814 8 : _userProfilesBox.put(
1815 : userId,
1816 4 : profile.toJson(),
1817 : );
1818 : }
|