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