Line data Source code
1 : /*
2 : * Famedly Matrix SDK
3 : * Copyright (C) 2019, 2020, 2021 Famedly GmbH
4 : *
5 : * This program is free software: you can redistribute it and/or modify
6 : * it under the terms of the GNU Affero General Public License as
7 : * published by the Free Software Foundation, either version 3 of the
8 : * License, or (at your option) any later version.
9 : *
10 : * This program is distributed in the hope that it will be useful,
11 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : * GNU Affero General Public License for more details.
14 : *
15 : * You should have received a copy of the GNU Affero General Public License
16 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 : */
18 :
19 : import 'dart:async';
20 : import 'dart:convert';
21 : import 'dart:math';
22 :
23 : import 'package:sqflite_common/sqflite.dart';
24 :
25 : import 'package:matrix/encryption/utils/olm_session.dart';
26 : import 'package:matrix/encryption/utils/outbound_group_session.dart';
27 : import 'package:matrix/encryption/utils/ssss_cache.dart';
28 : import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
29 : import 'package:matrix/matrix.dart';
30 : import 'package:matrix/src/utils/copy_map.dart';
31 : import 'package:matrix/src/utils/queued_to_device_event.dart';
32 : import 'package:matrix/src/utils/run_benchmarked.dart';
33 :
34 : import 'package:matrix/src/database/indexeddb_box.dart'
35 : if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart';
36 :
37 : import 'package:matrix/src/database/database_file_storage_stub.dart'
38 : if (dart.library.io) 'package:matrix/src/database/database_file_storage_io.dart';
39 :
40 : /// Database based on SQlite3 on native and IndexedDB on web. For native you
41 : /// have to pass a `Database` object, which can be created with the sqflite
42 : /// package like this:
43 : /// ```dart
44 : /// final database = await openDatabase('path/to/your/database');
45 : /// ```
46 : ///
47 : /// **WARNING**: For android it seems like that the CursorWindow is too small for
48 : /// large amounts of data if you are using SQFlite. Consider using a different
49 : /// package to open the database like
50 : /// [sqflite_sqlcipher](https://pub.dev/packages/sqflite_sqlcipher) or
51 : /// [sqflite_common_ffi](https://pub.dev/packages/sqflite_common_ffi).
52 : /// Learn more at:
53 : /// https://github.com/famedly/matrix-dart-sdk/issues/1642#issuecomment-1865827227
54 : class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
55 : static const int version = 9;
56 : final String name;
57 :
58 : late BoxCollection _collection;
59 : late Box<String> _clientBox;
60 : late Box<Map> _accountDataBox;
61 : late Box<Map> _roomsBox;
62 : late Box<Map> _toDeviceQueueBox;
63 :
64 : /// Key is a tuple as TupleKey(roomId, type) where stateKey can be
65 : /// an empty string. Must contain only states of type
66 : /// client.importantRoomStates.
67 : late Box<Map> _preloadRoomStateBox;
68 :
69 : /// Key is a tuple as TupleKey(roomId, type) where stateKey can be
70 : /// an empty string. Must NOT contain states of a type from
71 : /// client.importantRoomStates.
72 : late Box<Map> _nonPreloadRoomStateBox;
73 :
74 : /// Key is a tuple as TupleKey(roomId, userId)
75 : late Box<Map> _roomMembersBox;
76 :
77 : /// Key is a tuple as TupleKey(roomId, type)
78 : late Box<Map> _roomAccountDataBox;
79 : late Box<Map> _inboundGroupSessionsBox;
80 : late Box<String> _inboundGroupSessionsUploadQueueBox;
81 : late Box<Map> _outboundGroupSessionsBox;
82 : late Box<Map> _olmSessionsBox;
83 :
84 : /// Key is a tuple as TupleKey(userId, deviceId)
85 : late Box<Map> _userDeviceKeysBox;
86 :
87 : /// Key is the user ID as a String
88 : late Box<bool> _userDeviceKeysOutdatedBox;
89 :
90 : /// Key is a tuple as TupleKey(userId, publicKey)
91 : late Box<Map> _userCrossSigningKeysBox;
92 : late Box<Map> _ssssCacheBox;
93 : late Box<Map> _presencesBox;
94 :
95 : /// Key is a tuple as Multikey(roomId, fragmentId) while the default
96 : /// fragmentId is an empty String
97 : late Box<List> _timelineFragmentsBox;
98 :
99 : /// Key is a tuple as TupleKey(roomId, eventId)
100 : late Box<Map> _eventsBox;
101 :
102 : /// Key is a tuple as TupleKey(userId, deviceId)
103 : late Box<String> _seenDeviceIdsBox;
104 :
105 : late Box<String> _seenDeviceKeysBox;
106 :
107 : late Box<Map> _userProfilesBox;
108 :
109 : @override
110 : final int maxFileSize;
111 :
112 : // there was a field of type `dart:io:Directory` here. This one broke the
113 : // dart js standalone compiler. Migration via URI as file system identifier.
114 0 : @Deprecated(
115 : 'Breaks support for web standalone. Use [fileStorageLocation] instead.',
116 : )
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 4 : final dbKeys = client.importantStateEvents
597 16 : .map((state) => TupleKey(roomId, state).toString())
598 4 : .toList();
599 8 : final rawStates = await _preloadRoomStateBox.getAll(dbKeys);
600 8 : for (final rawState in rawStates) {
601 2 : if (rawState == null || rawState[''] == null) continue;
602 8 : room.setState(Event.fromJson(copyMap(rawState['']), room));
603 : }
604 : }
605 :
606 : return room;
607 : }
608 :
609 32 : @override
610 : Future<List<Room>> getRoomList(Client client) =>
611 64 : runBenchmarked<List<Room>>('Get room list from store', () async {
612 32 : final rooms = <String, Room>{};
613 :
614 64 : final rawRooms = await _roomsBox.getAllValues();
615 :
616 34 : for (final raw in rawRooms.values) {
617 : // Get the room
618 4 : final room = Room.fromJson(copyMap(raw), client);
619 :
620 : // Add to the list and continue.
621 4 : rooms[room.id] = room;
622 : }
623 :
624 64 : final roomStatesDataRaws = await _preloadRoomStateBox.getAllValues();
625 33 : for (final entry in roomStatesDataRaws.entries) {
626 2 : final keys = TupleKey.fromString(entry.key);
627 2 : final roomId = keys.parts.first;
628 1 : final room = rooms[roomId];
629 : if (room == null) {
630 0 : Logs().w('Found event in store for unknown room', entry.value);
631 : continue;
632 : }
633 1 : final states = entry.value;
634 1 : final stateEvents = states.values
635 1 : .map(
636 3 : (raw) => room.membership == Membership.invite
637 2 : ? StrippedStateEvent.fromJson(copyMap(raw))
638 2 : : Event.fromJson(copyMap(raw), room),
639 : )
640 1 : .toList();
641 2 : for (final state in stateEvents) {
642 1 : room.setState(state);
643 : }
644 : }
645 :
646 : // Get the room account data
647 64 : final roomAccountDataRaws = await _roomAccountDataBox.getAllValues();
648 33 : for (final entry in roomAccountDataRaws.entries) {
649 2 : final keys = TupleKey.fromString(entry.key);
650 1 : final basicRoomEvent = BasicRoomEvent.fromJson(
651 2 : copyMap(entry.value),
652 : );
653 2 : final roomId = keys.parts.first;
654 1 : if (rooms.containsKey(roomId)) {
655 4 : rooms[roomId]!.roomAccountData[basicRoomEvent.type] =
656 : basicRoomEvent;
657 : } else {
658 0 : Logs().w(
659 0 : 'Found account data for unknown room $roomId. Delete now...',
660 : );
661 0 : await _roomAccountDataBox
662 0 : .delete(TupleKey(roomId, basicRoomEvent.type).toString());
663 : }
664 : }
665 :
666 64 : return rooms.values.toList();
667 : });
668 :
669 24 : @override
670 : Future<SSSSCache?> getSSSSCache(String type) async {
671 48 : final raw = await _ssssCacheBox.get(type);
672 : if (raw == null) return null;
673 16 : return SSSSCache.fromJson(copyMap(raw));
674 : }
675 :
676 32 : @override
677 : Future<List<QueuedToDeviceEvent>> getToDeviceEventQueue() async {
678 64 : final raws = await _toDeviceQueueBox.getAllValues();
679 66 : final copiedRaws = raws.entries.map((entry) {
680 4 : final copiedRaw = copyMap(entry.value);
681 6 : copiedRaw['id'] = int.parse(entry.key);
682 6 : copiedRaw['content'] = jsonDecode(copiedRaw['content'] as String);
683 : return copiedRaw;
684 32 : }).toList();
685 68 : return copiedRaws.map((raw) => QueuedToDeviceEvent.fromJson(raw)).toList();
686 : }
687 :
688 6 : @override
689 : Future<List<Event>> getUnimportantRoomEventStatesForRoom(
690 : List<String> events,
691 : Room room,
692 : ) async {
693 21 : final keys = (await _nonPreloadRoomStateBox.getAllKeys()).where((key) {
694 3 : final tuple = TupleKey.fromString(key);
695 21 : return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
696 : });
697 :
698 6 : final unimportantEvents = <Event>[];
699 9 : for (final key in keys) {
700 6 : final states = await _nonPreloadRoomStateBox.get(key);
701 : if (states == null) continue;
702 3 : unimportantEvents.addAll(
703 15 : states.values.map((raw) => Event.fromJson(copyMap(raw), room)),
704 : );
705 : }
706 :
707 18 : return unimportantEvents.where((event) => event.stateKey != null).toList();
708 : }
709 :
710 32 : @override
711 : Future<User?> getUser(String userId, Room room) async {
712 : final state =
713 160 : await _roomMembersBox.get(TupleKey(room.id, userId).toString());
714 : if (state == null) return null;
715 93 : return Event.fromJson(copyMap(state), room).asUser;
716 : }
717 :
718 32 : @override
719 : Future<Map<String, DeviceKeysList>> getUserDeviceKeys(Client client) =>
720 32 : runBenchmarked<Map<String, DeviceKeysList>>(
721 32 : 'Get all user device keys from store', () async {
722 : final deviceKeysOutdated =
723 64 : await _userDeviceKeysOutdatedBox.getAllValues();
724 32 : if (deviceKeysOutdated.isEmpty) {
725 32 : return {};
726 : }
727 1 : final res = <String, DeviceKeysList>{};
728 2 : final userDeviceKeys = await _userDeviceKeysBox.getAllValues();
729 : final userCrossSigningKeys =
730 2 : await _userCrossSigningKeysBox.getAllValues();
731 2 : for (final userId in deviceKeysOutdated.keys) {
732 3 : final deviceKeysBoxKeys = userDeviceKeys.keys.where((tuple) {
733 1 : final tupleKey = TupleKey.fromString(tuple);
734 3 : return tupleKey.parts.first == userId;
735 : });
736 : final crossSigningKeysBoxKeys =
737 3 : userCrossSigningKeys.keys.where((tuple) {
738 1 : final tupleKey = TupleKey.fromString(tuple);
739 3 : return tupleKey.parts.first == userId;
740 : });
741 1 : final childEntries = deviceKeysBoxKeys.map(
742 1 : (key) {
743 1 : final userDeviceKey = userDeviceKeys[key];
744 : if (userDeviceKey == null) return null;
745 1 : return copyMap(userDeviceKey);
746 : },
747 : );
748 1 : final crossSigningEntries = crossSigningKeysBoxKeys.map(
749 1 : (key) {
750 1 : final crossSigningKey = userCrossSigningKeys[key];
751 : if (crossSigningKey == null) return null;
752 1 : return copyMap(crossSigningKey);
753 : },
754 : );
755 2 : res[userId] = DeviceKeysList.fromDbJson(
756 1 : {
757 1 : 'client_id': client.id,
758 : 'user_id': userId,
759 1 : 'outdated': deviceKeysOutdated[userId],
760 : },
761 : childEntries
762 2 : .where((c) => c != null)
763 1 : .toList()
764 1 : .cast<Map<String, dynamic>>(),
765 : crossSigningEntries
766 2 : .where((c) => c != null)
767 1 : .toList()
768 1 : .cast<Map<String, dynamic>>(),
769 : client,
770 : );
771 : }
772 : return res;
773 : });
774 :
775 32 : @override
776 : Future<List<User>> getUsers(Room room) async {
777 32 : final users = <User>[];
778 64 : final keys = (await _roomMembersBox.getAllKeys())
779 218 : .where((key) => TupleKey.fromString(key).parts.first == room.id)
780 32 : .toList();
781 64 : final states = await _roomMembersBox.getAll(keys);
782 35 : states.removeWhere((state) => state == null);
783 35 : for (final state in states) {
784 12 : users.add(Event.fromJson(copyMap(state!), room).asUser);
785 : }
786 :
787 : return users;
788 : }
789 :
790 34 : @override
791 : Future<int> insertClient(
792 : String name,
793 : String homeserverUrl,
794 : String token,
795 : DateTime? tokenExpiresAt,
796 : String? refreshToken,
797 : String userId,
798 : String? deviceId,
799 : String? deviceName,
800 : String? prevBatch,
801 : String? olmAccount,
802 : ) async {
803 68 : await transaction(() async {
804 68 : await _clientBox.put('homeserver_url', homeserverUrl);
805 68 : await _clientBox.put('token', token);
806 : if (tokenExpiresAt == null) {
807 66 : await _clientBox.delete('token_expires_at');
808 : } else {
809 2 : await _clientBox.put(
810 : 'token_expires_at',
811 2 : tokenExpiresAt.millisecondsSinceEpoch.toString(),
812 : );
813 : }
814 : if (refreshToken == null) {
815 12 : await _clientBox.delete('refresh_token');
816 : } else {
817 64 : await _clientBox.put('refresh_token', refreshToken);
818 : }
819 68 : await _clientBox.put('user_id', userId);
820 : if (deviceId == null) {
821 4 : await _clientBox.delete('device_id');
822 : } else {
823 64 : await _clientBox.put('device_id', deviceId);
824 : }
825 : if (deviceName == null) {
826 4 : await _clientBox.delete('device_name');
827 : } else {
828 64 : await _clientBox.put('device_name', deviceName);
829 : }
830 : if (prevBatch == null) {
831 66 : await _clientBox.delete('prev_batch');
832 : } else {
833 4 : await _clientBox.put('prev_batch', prevBatch);
834 : }
835 : if (olmAccount == null) {
836 20 : await _clientBox.delete('olm_account');
837 : } else {
838 48 : await _clientBox.put('olm_account', olmAccount);
839 : }
840 68 : await _clientBox.delete('sync_filter_id');
841 : });
842 : return 0;
843 : }
844 :
845 2 : @override
846 : Future<int> insertIntoToDeviceQueue(
847 : String type,
848 : String txnId,
849 : String content,
850 : ) async {
851 4 : final id = DateTime.now().millisecondsSinceEpoch;
852 8 : await _toDeviceQueueBox.put(id.toString(), {
853 : 'type': type,
854 : 'txn_id': txnId,
855 : 'content': content,
856 : });
857 : return id;
858 : }
859 :
860 5 : @override
861 : Future<void> markInboundGroupSessionAsUploaded(
862 : String roomId,
863 : String sessionId,
864 : ) async {
865 10 : await _inboundGroupSessionsUploadQueueBox.delete(sessionId);
866 : return;
867 : }
868 :
869 2 : @override
870 : Future<void> markInboundGroupSessionsAsNeedingUpload() async {
871 4 : final keys = await _inboundGroupSessionsBox.getAllKeys();
872 4 : for (final sessionId in keys) {
873 2 : final raw = copyMap(
874 4 : await _inboundGroupSessionsBox.get(sessionId) ?? {},
875 : );
876 2 : if (raw.isEmpty) continue;
877 2 : final roomId = raw.tryGet<String>('room_id');
878 : if (roomId == null) continue;
879 4 : await _inboundGroupSessionsUploadQueueBox.put(sessionId, roomId);
880 : }
881 : return;
882 : }
883 :
884 10 : @override
885 : Future<void> removeEvent(String eventId, String roomId) async {
886 40 : await _eventsBox.delete(TupleKey(roomId, eventId).toString());
887 20 : final keys = await _timelineFragmentsBox.getAllKeys();
888 20 : for (final key in keys) {
889 10 : final multiKey = TupleKey.fromString(key);
890 30 : if (multiKey.parts.first != roomId) continue;
891 : final eventIds =
892 30 : List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
893 10 : final prevLength = eventIds.length;
894 30 : eventIds.removeWhere((id) => id == eventId);
895 20 : if (eventIds.length < prevLength) {
896 20 : await _timelineFragmentsBox.put(key, eventIds);
897 : }
898 : }
899 : return;
900 : }
901 :
902 2 : @override
903 : Future<void> removeOutboundGroupSession(String roomId) async {
904 4 : await _outboundGroupSessionsBox.delete(roomId);
905 : return;
906 : }
907 :
908 4 : @override
909 : Future<void> removeUserCrossSigningKey(
910 : String userId,
911 : String publicKey,
912 : ) async {
913 4 : await _userCrossSigningKeysBox
914 12 : .delete(TupleKey(userId, publicKey).toString());
915 : return;
916 : }
917 :
918 1 : @override
919 : Future<void> removeUserDeviceKey(String userId, String deviceId) async {
920 4 : await _userDeviceKeysBox.delete(TupleKey(userId, deviceId).toString());
921 : return;
922 : }
923 :
924 3 : @override
925 : Future<void> setBlockedUserCrossSigningKey(
926 : bool blocked,
927 : String userId,
928 : String publicKey,
929 : ) async {
930 3 : final raw = copyMap(
931 3 : await _userCrossSigningKeysBox
932 9 : .get(TupleKey(userId, publicKey).toString()) ??
933 0 : {},
934 : );
935 3 : raw['blocked'] = blocked;
936 6 : await _userCrossSigningKeysBox.put(
937 6 : TupleKey(userId, publicKey).toString(),
938 : raw,
939 : );
940 : return;
941 : }
942 :
943 3 : @override
944 : Future<void> setBlockedUserDeviceKey(
945 : bool blocked,
946 : String userId,
947 : String deviceId,
948 : ) async {
949 3 : final raw = copyMap(
950 12 : await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
951 : );
952 3 : raw['blocked'] = blocked;
953 6 : await _userDeviceKeysBox.put(
954 6 : TupleKey(userId, deviceId).toString(),
955 : raw,
956 : );
957 : return;
958 : }
959 :
960 1 : @override
961 : Future<void> setLastActiveUserDeviceKey(
962 : int lastActive,
963 : String userId,
964 : String deviceId,
965 : ) async {
966 1 : final raw = copyMap(
967 4 : await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
968 : );
969 :
970 1 : raw['last_active'] = lastActive;
971 2 : await _userDeviceKeysBox.put(
972 2 : TupleKey(userId, deviceId).toString(),
973 : raw,
974 : );
975 : }
976 :
977 7 : @override
978 : Future<void> setLastSentMessageUserDeviceKey(
979 : String lastSentMessage,
980 : String userId,
981 : String deviceId,
982 : ) async {
983 7 : final raw = copyMap(
984 28 : await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
985 : );
986 7 : raw['last_sent_message'] = lastSentMessage;
987 14 : await _userDeviceKeysBox.put(
988 14 : TupleKey(userId, deviceId).toString(),
989 : raw,
990 : );
991 : }
992 :
993 2 : @override
994 : Future<void> setRoomPrevBatch(
995 : String? prevBatch,
996 : String roomId,
997 : Client client,
998 : ) async {
999 4 : final raw = await _roomsBox.get(roomId);
1000 : if (raw == null) return;
1001 2 : final room = Room.fromJson(copyMap(raw), client);
1002 1 : room.prev_batch = prevBatch;
1003 3 : await _roomsBox.put(roomId, room.toJson());
1004 : return;
1005 : }
1006 :
1007 6 : @override
1008 : Future<void> setVerifiedUserCrossSigningKey(
1009 : bool verified,
1010 : String userId,
1011 : String publicKey,
1012 : ) async {
1013 6 : final raw = copyMap(
1014 6 : (await _userCrossSigningKeysBox
1015 18 : .get(TupleKey(userId, publicKey).toString())) ??
1016 1 : {},
1017 : );
1018 6 : raw['verified'] = verified;
1019 12 : await _userCrossSigningKeysBox.put(
1020 12 : TupleKey(userId, publicKey).toString(),
1021 : raw,
1022 : );
1023 : return;
1024 : }
1025 :
1026 4 : @override
1027 : Future<void> setVerifiedUserDeviceKey(
1028 : bool verified,
1029 : String userId,
1030 : String deviceId,
1031 : ) async {
1032 4 : final raw = copyMap(
1033 16 : await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
1034 : );
1035 4 : raw['verified'] = verified;
1036 8 : await _userDeviceKeysBox.put(
1037 8 : TupleKey(userId, deviceId).toString(),
1038 : raw,
1039 : );
1040 : return;
1041 : }
1042 :
1043 32 : @override
1044 : Future<void> storeAccountData(String type, String content) async {
1045 96 : await _accountDataBox.put(type, jsonDecode(content));
1046 : return;
1047 : }
1048 :
1049 34 : @override
1050 : Future<void> storeEventUpdate(EventUpdate eventUpdate, Client client) async {
1051 : // Ephemerals should not be stored
1052 68 : if (eventUpdate.type == EventUpdateType.ephemeral) return;
1053 68 : final tmpRoom = client.getRoomById(eventUpdate.roomID) ??
1054 12 : Room(id: eventUpdate.roomID, client: client);
1055 :
1056 : // In case of this is a redaction event
1057 102 : if (eventUpdate.content['type'] == EventTypes.Redaction) {
1058 4 : final eventId = eventUpdate.content.tryGet<String>('redacts');
1059 : final event =
1060 0 : eventId != null ? await getEventById(eventId, tmpRoom) : null;
1061 : if (event != null) {
1062 0 : event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
1063 0 : await _eventsBox.put(
1064 0 : TupleKey(eventUpdate.roomID, event.eventId).toString(),
1065 0 : event.toJson(),
1066 : );
1067 :
1068 0 : if (tmpRoom.lastEvent?.eventId == event.eventId) {
1069 0 : if (client.importantStateEvents.contains(event.type)) {
1070 0 : await _preloadRoomStateBox.put(
1071 0 : TupleKey(eventUpdate.roomID, event.type).toString(),
1072 0 : {'': event.toJson()},
1073 : );
1074 : } else {
1075 0 : await _nonPreloadRoomStateBox.put(
1076 0 : TupleKey(eventUpdate.roomID, event.type).toString(),
1077 0 : {'': event.toJson()},
1078 : );
1079 : }
1080 : }
1081 : }
1082 : }
1083 :
1084 : // Store a common message event
1085 68 : if ({EventUpdateType.timeline, EventUpdateType.history}
1086 68 : .contains(eventUpdate.type)) {
1087 68 : final eventId = eventUpdate.content['event_id'];
1088 : // Is this ID already in the store?
1089 34 : final prevEvent = await _eventsBox
1090 136 : .get(TupleKey(eventUpdate.roomID, eventId).toString());
1091 : final prevStatus = prevEvent == null
1092 : ? null
1093 10 : : () {
1094 10 : final json = copyMap(prevEvent);
1095 10 : final statusInt = json.tryGet<int>('status') ??
1096 : json
1097 0 : .tryGetMap<String, dynamic>('unsigned')
1098 0 : ?.tryGet<int>(messageSendingStatusKey);
1099 10 : return statusInt == null ? null : eventStatusFromInt(statusInt);
1100 10 : }();
1101 :
1102 : // calculate the status
1103 34 : final newStatus = eventStatusFromInt(
1104 68 : eventUpdate.content.tryGet<int>('status') ??
1105 34 : eventUpdate.content
1106 34 : .tryGetMap<String, dynamic>('unsigned')
1107 31 : ?.tryGet<int>(messageSendingStatusKey) ??
1108 34 : EventStatus.synced.intValue,
1109 : );
1110 :
1111 : // Is this the response to a sending event which is already synced? Then
1112 : // there is nothing to do here.
1113 41 : if (!newStatus.isSynced && prevStatus != null && prevStatus.isSynced) {
1114 : return;
1115 : }
1116 :
1117 34 : final status = newStatus.isError || prevStatus == null
1118 : ? newStatus
1119 8 : : latestEventStatus(
1120 : prevStatus,
1121 : newStatus,
1122 : );
1123 :
1124 : // Add the status and the sort order to the content so it get stored
1125 102 : eventUpdate.content['unsigned'] ??= <String, dynamic>{};
1126 102 : eventUpdate.content['unsigned'][messageSendingStatusKey] =
1127 102 : eventUpdate.content['status'] = status.intValue;
1128 :
1129 : // In case this event has sent from this account we have a transaction ID
1130 34 : final transactionId = eventUpdate.content
1131 34 : .tryGetMap<String, dynamic>('unsigned')
1132 34 : ?.tryGet<String>('transaction_id');
1133 68 : await _eventsBox.put(
1134 102 : TupleKey(eventUpdate.roomID, eventId).toString(),
1135 34 : eventUpdate.content,
1136 : );
1137 :
1138 : // Update timeline fragments
1139 102 : final key = TupleKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
1140 34 : .toString();
1141 :
1142 : final eventIds =
1143 136 : List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
1144 :
1145 34 : if (!eventIds.contains(eventId)) {
1146 68 : if (eventUpdate.type == EventUpdateType.history) {
1147 2 : eventIds.add(eventId);
1148 : } else {
1149 34 : eventIds.insert(0, eventId);
1150 : }
1151 68 : await _timelineFragmentsBox.put(key, eventIds);
1152 8 : } else if (status.isSynced &&
1153 : prevStatus != null &&
1154 5 : prevStatus.isSent &&
1155 10 : eventUpdate.type != EventUpdateType.history) {
1156 : // Status changes from 1 -> 2? Make sure event is correctly sorted.
1157 5 : eventIds.remove(eventId);
1158 5 : eventIds.insert(0, eventId);
1159 : }
1160 :
1161 : // If event comes from server timeline, remove sending events with this ID
1162 34 : if (status.isSent) {
1163 102 : final key = TupleKey(eventUpdate.roomID, 'SENDING').toString();
1164 : final eventIds =
1165 136 : List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
1166 52 : final i = eventIds.indexWhere((id) => id == eventId);
1167 68 : if (i != -1) {
1168 6 : await _timelineFragmentsBox.put(key, eventIds..removeAt(i));
1169 : }
1170 : }
1171 :
1172 : // Is there a transaction id? Then delete the event with this id.
1173 68 : if (!status.isError && !status.isSending && transactionId != null) {
1174 18 : await removeEvent(transactionId, eventUpdate.roomID);
1175 : }
1176 : }
1177 :
1178 68 : final stateKey = eventUpdate.content['state_key'];
1179 : // Store a common state event
1180 : if (stateKey != null &&
1181 : // Don't store events as state updates when paginating backwards.
1182 64 : (eventUpdate.type == EventUpdateType.timeline ||
1183 64 : eventUpdate.type == EventUpdateType.state ||
1184 64 : eventUpdate.type == EventUpdateType.inviteState)) {
1185 96 : if (eventUpdate.content['type'] == EventTypes.RoomMember) {
1186 62 : await _roomMembersBox.put(
1187 31 : TupleKey(
1188 31 : eventUpdate.roomID,
1189 62 : eventUpdate.content['state_key'],
1190 31 : ).toString(),
1191 31 : eventUpdate.content,
1192 : );
1193 : } else {
1194 64 : final type = eventUpdate.content['type'] as String;
1195 64 : final roomStateBox = client.importantStateEvents.contains(type)
1196 32 : ? _preloadRoomStateBox
1197 31 : : _nonPreloadRoomStateBox;
1198 32 : final key = TupleKey(
1199 32 : eventUpdate.roomID,
1200 : type,
1201 32 : ).toString();
1202 96 : final stateMap = copyMap(await roomStateBox.get(key) ?? {});
1203 :
1204 64 : stateMap[stateKey] = eventUpdate.content;
1205 32 : await roomStateBox.put(key, stateMap);
1206 : }
1207 : }
1208 :
1209 : // Store a room account data event
1210 68 : if (eventUpdate.type == EventUpdateType.accountData) {
1211 62 : await _roomAccountDataBox.put(
1212 31 : TupleKey(
1213 31 : eventUpdate.roomID,
1214 62 : eventUpdate.content['type'],
1215 31 : ).toString(),
1216 31 : eventUpdate.content,
1217 : );
1218 : }
1219 : }
1220 :
1221 24 : @override
1222 : Future<void> storeInboundGroupSession(
1223 : String roomId,
1224 : String sessionId,
1225 : String pickle,
1226 : String content,
1227 : String indexes,
1228 : String allowedAtIndex,
1229 : String senderKey,
1230 : String senderClaimedKey,
1231 : ) async {
1232 24 : final json = StoredInboundGroupSession(
1233 : roomId: roomId,
1234 : sessionId: sessionId,
1235 : pickle: pickle,
1236 : content: content,
1237 : indexes: indexes,
1238 : allowedAtIndex: allowedAtIndex,
1239 : senderKey: senderKey,
1240 : senderClaimedKeys: senderClaimedKey,
1241 24 : ).toJson();
1242 48 : await _inboundGroupSessionsBox.put(
1243 : sessionId,
1244 : json,
1245 : );
1246 : // Mark this session as needing upload too
1247 48 : await _inboundGroupSessionsUploadQueueBox.put(sessionId, roomId);
1248 : return;
1249 : }
1250 :
1251 6 : @override
1252 : Future<void> storeOutboundGroupSession(
1253 : String roomId,
1254 : String pickle,
1255 : String deviceIds,
1256 : int creationTime,
1257 : ) async {
1258 18 : await _outboundGroupSessionsBox.put(roomId, <String, dynamic>{
1259 : 'room_id': roomId,
1260 : 'pickle': pickle,
1261 : 'device_ids': deviceIds,
1262 : 'creation_time': creationTime,
1263 : });
1264 : return;
1265 : }
1266 :
1267 31 : @override
1268 : Future<void> storePrevBatch(
1269 : String prevBatch,
1270 : ) async {
1271 93 : if ((await _clientBox.getAllKeys()).isEmpty) return;
1272 62 : await _clientBox.put('prev_batch', prevBatch);
1273 : return;
1274 : }
1275 :
1276 32 : @override
1277 : Future<void> storeRoomUpdate(
1278 : String roomId,
1279 : SyncRoomUpdate roomUpdate,
1280 : Event? lastEvent,
1281 : Client client,
1282 : ) async {
1283 : // Leave room if membership is leave
1284 32 : if (roomUpdate is LeftRoomUpdate) {
1285 31 : await forgetRoom(roomId);
1286 : return;
1287 : }
1288 32 : final membership = roomUpdate is LeftRoomUpdate
1289 : ? Membership.leave
1290 32 : : roomUpdate is InvitedRoomUpdate
1291 : ? Membership.invite
1292 : : Membership.join;
1293 : // Make sure room exists
1294 64 : final currentRawRoom = await _roomsBox.get(roomId);
1295 : if (currentRawRoom == null) {
1296 64 : await _roomsBox.put(
1297 : roomId,
1298 32 : roomUpdate is JoinedRoomUpdate
1299 32 : ? Room(
1300 : client: client,
1301 : id: roomId,
1302 : membership: membership,
1303 : highlightCount:
1304 94 : roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
1305 : 0,
1306 : notificationCount: roomUpdate
1307 63 : .unreadNotifications?.notificationCount
1308 31 : ?.toInt() ??
1309 : 0,
1310 63 : prev_batch: roomUpdate.timeline?.prevBatch,
1311 32 : summary: roomUpdate.summary,
1312 : lastEvent: lastEvent,
1313 32 : ).toJson()
1314 31 : : Room(
1315 : client: client,
1316 : id: roomId,
1317 : membership: membership,
1318 : lastEvent: lastEvent,
1319 31 : ).toJson(),
1320 : );
1321 11 : } else if (roomUpdate is JoinedRoomUpdate) {
1322 22 : final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
1323 22 : await _roomsBox.put(
1324 : roomId,
1325 11 : Room(
1326 : client: client,
1327 : id: roomId,
1328 : membership: membership,
1329 : highlightCount:
1330 13 : roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
1331 11 : currentRoom.highlightCount,
1332 : notificationCount:
1333 13 : roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
1334 11 : currentRoom.notificationCount,
1335 32 : prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
1336 11 : summary: RoomSummary.fromJson(
1337 22 : currentRoom.summary.toJson()
1338 34 : ..addAll(roomUpdate.summary?.toJson() ?? {}),
1339 : ),
1340 : lastEvent: lastEvent,
1341 11 : ).toJson(),
1342 : );
1343 : }
1344 : }
1345 :
1346 31 : @override
1347 : Future<void> deleteTimelineForRoom(String roomId) =>
1348 124 : _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
1349 :
1350 8 : @override
1351 : Future<void> storeSSSSCache(
1352 : String type,
1353 : String keyId,
1354 : String ciphertext,
1355 : String content,
1356 : ) async {
1357 16 : await _ssssCacheBox.put(
1358 : type,
1359 8 : SSSSCache(
1360 : type: type,
1361 : keyId: keyId,
1362 : ciphertext: ciphertext,
1363 : content: content,
1364 8 : ).toJson(),
1365 : );
1366 : }
1367 :
1368 32 : @override
1369 : Future<void> storeSyncFilterId(
1370 : String syncFilterId,
1371 : ) async {
1372 64 : await _clientBox.put('sync_filter_id', syncFilterId);
1373 : }
1374 :
1375 32 : @override
1376 : Future<void> storeUserCrossSigningKey(
1377 : String userId,
1378 : String publicKey,
1379 : String content,
1380 : bool verified,
1381 : bool blocked,
1382 : ) async {
1383 64 : await _userCrossSigningKeysBox.put(
1384 64 : TupleKey(userId, publicKey).toString(),
1385 32 : {
1386 : 'user_id': userId,
1387 : 'public_key': publicKey,
1388 : 'content': content,
1389 : 'verified': verified,
1390 : 'blocked': blocked,
1391 : },
1392 : );
1393 : }
1394 :
1395 32 : @override
1396 : Future<void> storeUserDeviceKey(
1397 : String userId,
1398 : String deviceId,
1399 : String content,
1400 : bool verified,
1401 : bool blocked,
1402 : int lastActive,
1403 : ) async {
1404 160 : await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
1405 : 'user_id': userId,
1406 : 'device_id': deviceId,
1407 : 'content': content,
1408 : 'verified': verified,
1409 : 'blocked': blocked,
1410 : 'last_active': lastActive,
1411 : 'last_sent_message': '',
1412 : });
1413 : return;
1414 : }
1415 :
1416 32 : @override
1417 : Future<void> storeUserDeviceKeysInfo(String userId, bool outdated) async {
1418 64 : await _userDeviceKeysOutdatedBox.put(userId, outdated);
1419 : return;
1420 : }
1421 :
1422 34 : @override
1423 : Future<void> transaction(Future<void> Function() action) =>
1424 68 : _collection.transaction(action);
1425 :
1426 2 : @override
1427 : Future<void> updateClient(
1428 : String homeserverUrl,
1429 : String token,
1430 : DateTime? tokenExpiresAt,
1431 : String? refreshToken,
1432 : String userId,
1433 : String? deviceId,
1434 : String? deviceName,
1435 : String? prevBatch,
1436 : String? olmAccount,
1437 : ) async {
1438 4 : await transaction(() async {
1439 4 : await _clientBox.put('homeserver_url', homeserverUrl);
1440 4 : await _clientBox.put('token', token);
1441 : if (tokenExpiresAt == null) {
1442 0 : await _clientBox.delete('token_expires_at');
1443 : } else {
1444 4 : await _clientBox.put(
1445 : 'token_expires_at',
1446 4 : tokenExpiresAt.millisecondsSinceEpoch.toString(),
1447 : );
1448 : }
1449 : if (refreshToken == null) {
1450 0 : await _clientBox.delete('refresh_token');
1451 : } else {
1452 4 : await _clientBox.put('refresh_token', refreshToken);
1453 : }
1454 4 : await _clientBox.put('user_id', userId);
1455 : if (deviceId == null) {
1456 0 : await _clientBox.delete('device_id');
1457 : } else {
1458 4 : await _clientBox.put('device_id', deviceId);
1459 : }
1460 : if (deviceName == null) {
1461 0 : await _clientBox.delete('device_name');
1462 : } else {
1463 4 : await _clientBox.put('device_name', deviceName);
1464 : }
1465 : if (prevBatch == null) {
1466 0 : await _clientBox.delete('prev_batch');
1467 : } else {
1468 4 : await _clientBox.put('prev_batch', prevBatch);
1469 : }
1470 : if (olmAccount == null) {
1471 0 : await _clientBox.delete('olm_account');
1472 : } else {
1473 4 : await _clientBox.put('olm_account', olmAccount);
1474 : }
1475 : });
1476 : return;
1477 : }
1478 :
1479 24 : @override
1480 : Future<void> updateClientKeys(
1481 : String olmAccount,
1482 : ) async {
1483 48 : await _clientBox.put('olm_account', olmAccount);
1484 : return;
1485 : }
1486 :
1487 2 : @override
1488 : Future<void> updateInboundGroupSessionAllowedAtIndex(
1489 : String allowedAtIndex,
1490 : String roomId,
1491 : String sessionId,
1492 : ) async {
1493 4 : final raw = await _inboundGroupSessionsBox.get(sessionId);
1494 : if (raw == null) {
1495 0 : Logs().w(
1496 : 'Tried to update inbound group session as uploaded which wasnt found in the database!',
1497 : );
1498 : return;
1499 : }
1500 2 : raw['allowed_at_index'] = allowedAtIndex;
1501 4 : await _inboundGroupSessionsBox.put(sessionId, raw);
1502 : return;
1503 : }
1504 :
1505 4 : @override
1506 : Future<void> updateInboundGroupSessionIndexes(
1507 : String indexes,
1508 : String roomId,
1509 : String sessionId,
1510 : ) async {
1511 8 : final raw = await _inboundGroupSessionsBox.get(sessionId);
1512 : if (raw == null) {
1513 0 : Logs().w(
1514 : 'Tried to update inbound group session indexes of a session which was not found in the database!',
1515 : );
1516 : return;
1517 : }
1518 4 : final json = copyMap(raw);
1519 4 : json['indexes'] = indexes;
1520 8 : await _inboundGroupSessionsBox.put(sessionId, json);
1521 : return;
1522 : }
1523 :
1524 2 : @override
1525 : Future<List<StoredInboundGroupSession>> getAllInboundGroupSessions() async {
1526 4 : final rawSessions = await _inboundGroupSessionsBox.getAllValues();
1527 2 : return rawSessions.values
1528 5 : .map((raw) => StoredInboundGroupSession.fromJson(copyMap(raw)))
1529 2 : .toList();
1530 : }
1531 :
1532 31 : @override
1533 : Future<void> addSeenDeviceId(
1534 : String userId,
1535 : String deviceId,
1536 : String publicKeys,
1537 : ) =>
1538 124 : _seenDeviceIdsBox.put(TupleKey(userId, deviceId).toString(), publicKeys);
1539 :
1540 31 : @override
1541 : Future<void> addSeenPublicKey(
1542 : String publicKey,
1543 : String deviceId,
1544 : ) =>
1545 62 : _seenDeviceKeysBox.put(publicKey, deviceId);
1546 :
1547 31 : @override
1548 : Future<String?> deviceIdSeen(userId, deviceId) async {
1549 : final raw =
1550 124 : await _seenDeviceIdsBox.get(TupleKey(userId, deviceId).toString());
1551 : if (raw == null) return null;
1552 : return raw;
1553 : }
1554 :
1555 31 : @override
1556 : Future<String?> publicKeySeen(String publicKey) async {
1557 62 : final raw = await _seenDeviceKeysBox.get(publicKey);
1558 : if (raw == null) return null;
1559 : return raw;
1560 : }
1561 :
1562 0 : @override
1563 : Future<String> exportDump() async {
1564 0 : final dataMap = {
1565 0 : _clientBoxName: await _clientBox.getAllValues(),
1566 0 : _accountDataBoxName: await _accountDataBox.getAllValues(),
1567 0 : _roomsBoxName: await _roomsBox.getAllValues(),
1568 0 : _preloadRoomStateBoxName: await _preloadRoomStateBox.getAllValues(),
1569 0 : _nonPreloadRoomStateBoxName: await _nonPreloadRoomStateBox.getAllValues(),
1570 0 : _roomMembersBoxName: await _roomMembersBox.getAllValues(),
1571 0 : _toDeviceQueueBoxName: await _toDeviceQueueBox.getAllValues(),
1572 0 : _roomAccountDataBoxName: await _roomAccountDataBox.getAllValues(),
1573 : _inboundGroupSessionsBoxName:
1574 0 : await _inboundGroupSessionsBox.getAllValues(),
1575 : _inboundGroupSessionsUploadQueueBoxName:
1576 0 : await _inboundGroupSessionsUploadQueueBox.getAllValues(),
1577 : _outboundGroupSessionsBoxName:
1578 0 : await _outboundGroupSessionsBox.getAllValues(),
1579 0 : _olmSessionsBoxName: await _olmSessionsBox.getAllValues(),
1580 0 : _userDeviceKeysBoxName: await _userDeviceKeysBox.getAllValues(),
1581 : _userDeviceKeysOutdatedBoxName:
1582 0 : await _userDeviceKeysOutdatedBox.getAllValues(),
1583 : _userCrossSigningKeysBoxName:
1584 0 : await _userCrossSigningKeysBox.getAllValues(),
1585 0 : _ssssCacheBoxName: await _ssssCacheBox.getAllValues(),
1586 0 : _presencesBoxName: await _presencesBox.getAllValues(),
1587 0 : _timelineFragmentsBoxName: await _timelineFragmentsBox.getAllValues(),
1588 0 : _eventsBoxName: await _eventsBox.getAllValues(),
1589 0 : _seenDeviceIdsBoxName: await _seenDeviceIdsBox.getAllValues(),
1590 0 : _seenDeviceKeysBoxName: await _seenDeviceKeysBox.getAllValues(),
1591 : };
1592 0 : final json = jsonEncode(dataMap);
1593 0 : await clear();
1594 : return json;
1595 : }
1596 :
1597 0 : @override
1598 : Future<bool> importDump(String export) async {
1599 : try {
1600 0 : await clear();
1601 0 : await open();
1602 0 : final json = Map.from(jsonDecode(export)).cast<String, Map>();
1603 0 : for (final key in json[_clientBoxName]!.keys) {
1604 0 : await _clientBox.put(key, json[_clientBoxName]![key]);
1605 : }
1606 0 : for (final key in json[_accountDataBoxName]!.keys) {
1607 0 : await _accountDataBox.put(key, json[_accountDataBoxName]![key]);
1608 : }
1609 0 : for (final key in json[_roomsBoxName]!.keys) {
1610 0 : await _roomsBox.put(key, json[_roomsBoxName]![key]);
1611 : }
1612 0 : for (final key in json[_preloadRoomStateBoxName]!.keys) {
1613 0 : await _preloadRoomStateBox.put(
1614 : key,
1615 0 : json[_preloadRoomStateBoxName]![key],
1616 : );
1617 : }
1618 0 : for (final key in json[_nonPreloadRoomStateBoxName]!.keys) {
1619 0 : await _nonPreloadRoomStateBox.put(
1620 : key,
1621 0 : json[_nonPreloadRoomStateBoxName]![key],
1622 : );
1623 : }
1624 0 : for (final key in json[_roomMembersBoxName]!.keys) {
1625 0 : await _roomMembersBox.put(key, json[_roomMembersBoxName]![key]);
1626 : }
1627 0 : for (final key in json[_toDeviceQueueBoxName]!.keys) {
1628 0 : await _toDeviceQueueBox.put(key, json[_toDeviceQueueBoxName]![key]);
1629 : }
1630 0 : for (final key in json[_roomAccountDataBoxName]!.keys) {
1631 0 : await _roomAccountDataBox.put(key, json[_roomAccountDataBoxName]![key]);
1632 : }
1633 0 : for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
1634 0 : await _inboundGroupSessionsBox.put(
1635 : key,
1636 0 : json[_inboundGroupSessionsBoxName]![key],
1637 : );
1638 : }
1639 0 : for (final key in json[_inboundGroupSessionsUploadQueueBoxName]!.keys) {
1640 0 : await _inboundGroupSessionsUploadQueueBox.put(
1641 : key,
1642 0 : json[_inboundGroupSessionsUploadQueueBoxName]![key],
1643 : );
1644 : }
1645 0 : for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
1646 0 : await _outboundGroupSessionsBox.put(
1647 : key,
1648 0 : json[_outboundGroupSessionsBoxName]![key],
1649 : );
1650 : }
1651 0 : for (final key in json[_olmSessionsBoxName]!.keys) {
1652 0 : await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
1653 : }
1654 0 : for (final key in json[_userDeviceKeysBoxName]!.keys) {
1655 0 : await _userDeviceKeysBox.put(key, json[_userDeviceKeysBoxName]![key]);
1656 : }
1657 0 : for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
1658 0 : await _userDeviceKeysOutdatedBox.put(
1659 : key,
1660 0 : json[_userDeviceKeysOutdatedBoxName]![key],
1661 : );
1662 : }
1663 0 : for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
1664 0 : await _userCrossSigningKeysBox.put(
1665 : key,
1666 0 : json[_userCrossSigningKeysBoxName]![key],
1667 : );
1668 : }
1669 0 : for (final key in json[_ssssCacheBoxName]!.keys) {
1670 0 : await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
1671 : }
1672 0 : for (final key in json[_presencesBoxName]!.keys) {
1673 0 : await _presencesBox.put(key, json[_presencesBoxName]![key]);
1674 : }
1675 0 : for (final key in json[_timelineFragmentsBoxName]!.keys) {
1676 0 : await _timelineFragmentsBox.put(
1677 : key,
1678 0 : json[_timelineFragmentsBoxName]![key],
1679 : );
1680 : }
1681 0 : for (final key in json[_seenDeviceIdsBoxName]!.keys) {
1682 0 : await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]);
1683 : }
1684 0 : for (final key in json[_seenDeviceKeysBoxName]!.keys) {
1685 0 : await _seenDeviceKeysBox.put(key, json[_seenDeviceKeysBoxName]![key]);
1686 : }
1687 : return true;
1688 : } catch (e, s) {
1689 0 : Logs().e('Database import error: ', e, s);
1690 : return false;
1691 : }
1692 : }
1693 :
1694 1 : @override
1695 : Future<List<String>> getEventIdList(
1696 : Room room, {
1697 : int start = 0,
1698 : bool includeSending = false,
1699 : int? limit,
1700 : }) =>
1701 2 : runBenchmarked<List<String>>('Get event id list', () async {
1702 : // Get the synced event IDs from the store
1703 3 : final timelineKey = TupleKey(room.id, '').toString();
1704 1 : final timelineEventIds = List<String>.from(
1705 2 : (await _timelineFragmentsBox.get(timelineKey)) ?? [],
1706 : );
1707 :
1708 : // Get the local stored SENDING events from the store
1709 : late final List<String> sendingEventIds;
1710 : if (!includeSending) {
1711 1 : sendingEventIds = [];
1712 : } else {
1713 0 : final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
1714 0 : sendingEventIds = List<String>.from(
1715 0 : (await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
1716 : );
1717 : }
1718 :
1719 : // Combine those two lists while respecting the start and limit parameters.
1720 : // Create a new list object instead of concatonating list to prevent
1721 : // random type errors.
1722 1 : final eventIds = [
1723 : ...sendingEventIds,
1724 1 : ...timelineEventIds,
1725 : ];
1726 0 : if (limit != null && eventIds.length > limit) {
1727 0 : eventIds.removeRange(limit, eventIds.length);
1728 : }
1729 :
1730 : return eventIds;
1731 : });
1732 :
1733 32 : @override
1734 : Future<void> storePresence(String userId, CachedPresence presence) =>
1735 96 : _presencesBox.put(userId, presence.toJson());
1736 :
1737 1 : @override
1738 : Future<CachedPresence?> getPresence(String userId) async {
1739 2 : final rawPresence = await _presencesBox.get(userId);
1740 : if (rawPresence == null) return null;
1741 :
1742 2 : return CachedPresence.fromJson(copyMap(rawPresence));
1743 : }
1744 :
1745 1 : @override
1746 : Future<void> storeWellKnown(DiscoveryInformation? discoveryInformation) {
1747 : if (discoveryInformation == null) {
1748 2 : return _clientBox.delete('discovery_information');
1749 : }
1750 2 : return _clientBox.put(
1751 : 'discovery_information',
1752 2 : jsonEncode(discoveryInformation.toJson()),
1753 : );
1754 : }
1755 :
1756 31 : @override
1757 : Future<DiscoveryInformation?> getWellKnown() async {
1758 : final rawDiscoveryInformation =
1759 62 : await _clientBox.get('discovery_information');
1760 : if (rawDiscoveryInformation == null) return null;
1761 2 : return DiscoveryInformation.fromJson(jsonDecode(rawDiscoveryInformation));
1762 : }
1763 :
1764 9 : @override
1765 : Future<void> delete() async {
1766 : // database?.path is null on web
1767 18 : await _collection.deleteDatabase(
1768 18 : database?.path ?? name,
1769 9 : sqfliteFactory ?? idbFactory,
1770 : );
1771 : }
1772 :
1773 32 : @override
1774 : Future<void> markUserProfileAsOutdated(userId) async {
1775 32 : final profile = await getUserProfile(userId);
1776 : if (profile == null) return;
1777 4 : await _userProfilesBox.put(
1778 : userId,
1779 2 : CachedProfileInformation.fromProfile(
1780 : profile as ProfileInformation,
1781 : outdated: true,
1782 2 : updated: profile.updated,
1783 2 : ).toJson(),
1784 : );
1785 : }
1786 :
1787 32 : @override
1788 : Future<CachedProfileInformation?> getUserProfile(String userId) =>
1789 96 : _userProfilesBox.get(userId).then(
1790 32 : (json) => json == null
1791 : ? null
1792 4 : : CachedProfileInformation.fromJson(copyMap(json)),
1793 : );
1794 :
1795 4 : @override
1796 : Future<void> storeUserProfile(
1797 : String userId,
1798 : CachedProfileInformation profile,
1799 : ) =>
1800 8 : _userProfilesBox.put(
1801 : userId,
1802 4 : profile.toJson(),
1803 : );
1804 : }
|