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