/*
* Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:sembast/sembast.dart';
import 'package:matrix/encryption/utils/olm_session.dart';
import 'package:matrix/encryption/utils/outbound_group_session.dart';
import 'package:matrix/encryption/utils/ssss_cache.dart';
import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
import 'package:matrix/matrix.dart' hide Filter;
import 'package:matrix/src/event_status.dart';
import 'package:matrix/src/utils/queued_to_device_event.dart';
import 'package:matrix/src/utils/run_benchmarked.dart';
import 'package:sembast/sembast_memory.dart';
import 'package:sembast/utils/value_utils.dart';
/// Sembast implementation of the DatabaseAPI. You need to pass through the
/// correct dbfactory. By default it uses an in-memory database so there is no
/// persistent storage. Learn more on: https://pub.dev/packages/sembast
class MatrixSembastDatabase extends DatabaseApi {
static const int version = 5;
final String name;
final String path;
late final Database _database;
Transaction? _currentTransaction;
/// The transaction to use here. If there is a real transaction ongoing it
/// will use it and otherwise just use the default which is the database
/// object itself.
DatabaseClient get txn => (_transactionLock?.isCompleted ?? true)
? _database
: _currentTransaction ?? _database;
final DatabaseFactory _dbFactory;
late final StoreRef _clientBox = StoreRef(_clientBoxName);
late final StoreRef> _accountDataBox =
StoreRef(_accountDataBoxName);
late final StoreRef> _roomsBox =
StoreRef(_roomsBoxName);
late final StoreRef> _toDeviceQueueBox =
StoreRef(_toDeviceQueueBoxName);
/// Key is a tuple as SembastKey(roomId, type) where stateKey can be
/// an empty string.
late final StoreRef> _roomStateBox =
StoreRef(_roomStateBoxName);
/// Key is a tuple as SembastKey(roomId, userId)
late final StoreRef> _roomMembersBox =
StoreRef(_roomMembersBoxName);
/// Key is a tuple as SembastKey(roomId, type)
late final StoreRef> _roomAccountDataBox =
StoreRef(_roomAccountDataBoxName);
late final StoreRef> _inboundGroupSessionsBox =
StoreRef(_inboundGroupSessionsBoxName);
late final StoreRef> _outboundGroupSessionsBox =
StoreRef(_outboundGroupSessionsBoxName);
late final StoreRef> _olmSessionsBox =
StoreRef(_olmSessionsBoxName);
/// Key is a tuple as SembastKey(userId, deviceId)
late final StoreRef> _userDeviceKeysBox =
StoreRef(_userDeviceKeysBoxName);
/// Key is the user ID as a String
late final StoreRef _userDeviceKeysOutdatedBox =
StoreRef(_userDeviceKeysOutdatedBoxName);
/// Key is a tuple as SembastKey(userId, publicKey)
late final StoreRef> _userCrossSigningKeysBox =
StoreRef(_userCrossSigningKeysBoxName);
late final StoreRef> _ssssCacheBox =
StoreRef(_ssssCacheBoxName);
late final StoreRef> _presencesBox =
StoreRef(_presencesBoxName);
/// Key is a tuple as Multikey(roomId, fragmentId) while the default
/// fragmentId is an empty String
late final StoreRef> _timelineFragmentsBox =
StoreRef(_timelineFragmentsBoxName);
/// Key is a tuple as SembastKey(roomId, eventId)
late final StoreRef> _eventsBox =
StoreRef(_eventsBoxName);
/// Key is a tuple as SembastKey(userId, deviceId)
late final StoreRef _seenDeviceIdsBox =
StoreRef(_seenDeviceIdsBoxName);
late final StoreRef _seenDeviceKeysBox =
StoreRef(_seenDeviceKeysBoxName);
String get _clientBoxName => '$name.box.client';
String get _accountDataBoxName => '$name.box.account_data';
String get _roomsBoxName => '$name.box.rooms';
String get _toDeviceQueueBoxName => '$name.box.to_device_queue';
String get _roomStateBoxName => '$name.box.room_states';
String get _roomMembersBoxName => '$name.box.room_members';
String get _roomAccountDataBoxName => '$name.box.room_account_data';
String get _inboundGroupSessionsBoxName => '$name.box.inbound_group_session';
String get _outboundGroupSessionsBoxName =>
'$name.box.outbound_group_session';
String get _olmSessionsBoxName => '$name.box.olm_session';
String get _userDeviceKeysBoxName => '$name.box.user_device_keys';
String get _userDeviceKeysOutdatedBoxName =>
'$name.box.user_device_keys_outdated';
String get _userCrossSigningKeysBoxName => '$name.box.cross_signing_keys';
String get _ssssCacheBoxName => '$name.box.ssss_cache';
String get _presencesBoxName => '$name.box.presences';
String get _timelineFragmentsBoxName => '$name.box.timeline_fragments';
String get _eventsBoxName => '$name.box.events';
String get _seenDeviceIdsBoxName => '$name.box.seen_device_ids';
String get _seenDeviceKeysBoxName => '$name.box.seen_device_keys';
final SembastCodec? codec;
MatrixSembastDatabase(
this.name, {
this.path = './database.db',
this.codec,
DatabaseFactory? dbFactory,
}) : _dbFactory = dbFactory ?? databaseFactoryMemory;
@override
int get maxFileSize => 0;
Future _actionOnAllBoxes(Future Function(StoreRef box) action) =>
Future.wait([
action(_clientBox),
action(_accountDataBox),
action(_roomsBox),
action(_roomStateBox),
action(_roomMembersBox),
action(_toDeviceQueueBox),
action(_roomAccountDataBox),
action(_inboundGroupSessionsBox),
action(_outboundGroupSessionsBox),
action(_olmSessionsBox),
action(_userDeviceKeysBox),
action(_userDeviceKeysOutdatedBox),
action(_userCrossSigningKeysBox),
action(_ssssCacheBox),
action(_presencesBox),
action(_timelineFragmentsBox),
action(_eventsBox),
action(_seenDeviceIdsBox),
action(_seenDeviceKeysBox),
]);
Future open() async {
_database = await _dbFactory.openDatabase(path, codec: codec);
// Check version and check if we need a migration
final currentVersion =
(await _clientBox.record('version').get(txn) as int?);
if (currentVersion == null) {
await _clientBox.record('version').put(txn, version);
} else if (currentVersion != version) {
await _migrateFromVersion(currentVersion);
}
return;
}
Future _migrateFromVersion(int currentVersion) async {
Logs()
.i('Migrate Sembast database from version $currentVersion to $version');
if (version == 5) {
await _database.transaction((txn) async {
final keys = await _userDeviceKeysBox.findKeys(txn);
for (final key in keys) {
try {
final raw = await _userDeviceKeysBox.record(key).get(txn) as Map;
if (!raw.containsKey('keys')) continue;
final deviceKeys = DeviceKeys.fromJson(
cloneMap(raw),
Client(''),
);
await addSeenDeviceId(deviceKeys.userId, deviceKeys.deviceId!,
deviceKeys.curve25519Key! + deviceKeys.ed25519Key!);
await addSeenPublicKey(
deviceKeys.ed25519Key!, deviceKeys.deviceId!);
await addSeenPublicKey(
deviceKeys.curve25519Key!, deviceKeys.deviceId!);
} catch (e) {
Logs().w('Can not migrate device $key', e);
}
}
});
}
await clearCache();
await _clientBox.record('version').put(txn, version);
}
@override
Future clear() async {
Logs().i('Clear and close Sembast database...');
await _actionOnAllBoxes((box) => box.delete(txn));
return;
}
@override
Future clearCache() async {
await _roomsBox.delete(txn);
await _accountDataBox.delete(txn);
await _roomStateBox.delete(txn);
await _roomMembersBox.delete(txn);
await _eventsBox.delete(txn);
await _timelineFragmentsBox.delete(txn);
await _outboundGroupSessionsBox.delete(txn);
await _presencesBox.delete(txn);
await _clientBox.record('prev_batch').delete(txn);
}
@override
Future clearSSSSCache() async {
await _ssssCacheBox.delete(txn);
}
@override
Future close() async {
// We never close a sembast database
// https://github.com/tekartik/sembast.dart/issues/219
}
@override
Future deleteFromToDeviceQueue(int id) async {
await _toDeviceQueueBox.record(id).delete(txn);
return;
}
@override
Future deleteOldFiles(int savedAt) async {
return;
}
@override
Future forgetRoom(String roomId) async {
await _timelineFragmentsBox
.record(SembastKey(roomId, '').toString())
.delete(txn);
final eventKeys = await _eventsBox.findKeys(txn);
for (final key in eventKeys) {
final multiKey = SembastKey.fromString(key);
if (multiKey.parts.first != roomId) continue;
await _eventsBox.record(key).delete(txn);
}
final roomStateKeys = await _roomStateBox.findKeys(txn);
for (final key in roomStateKeys) {
final multiKey = SembastKey.fromString(key);
if (multiKey.parts.first != roomId) continue;
await _roomStateBox.record(key).delete(txn);
}
final roomMembersKeys = await _roomMembersBox.findKeys(txn);
for (final key in roomMembersKeys) {
final multiKey = SembastKey.fromString(key);
if (multiKey.parts.first != roomId) continue;
await _roomMembersBox.record(key).delete(txn);
}
final roomAccountData = await _roomAccountDataBox.findKeys(txn);
for (final key in roomAccountData) {
final multiKey = SembastKey.fromString(key);
if (multiKey.parts.first != roomId) continue;
await _roomAccountDataBox.record(key).delete(txn);
}
await _roomsBox.record(roomId).delete(txn);
}
@override
Future