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