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