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