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