Switch to moor

This commit is contained in:
Sorunome 2020-05-15 18:40:17 +00:00 committed by Christian Pauly
parent 729f1f3b78
commit f71826739c
27 changed files with 6645 additions and 658 deletions

1
.gitignore vendored
View file

@ -23,6 +23,7 @@ native/
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/

View file

@ -17,7 +17,7 @@ coverage:
- curl https://storage.googleapis.com/dart-archive/channels/stable/release/2.7.2/linux_packages/dart_2.7.2-1_amd64.deb > dart.deb
- apt install -y ./dart.deb
- apt update
- apt install -y chromium lcov libolm3
- apt install -y chromium lcov libolm3 sqlite3 libsqlite3-dev
- ln -s /usr/lib/dart/bin/pub /usr/bin/
- useradd -m test
- chown -R 'test:' '.'
@ -33,7 +33,7 @@ coverage_without_olm:
dependencies: []
script:
- apt update
- apt install -y curl git
- apt install -y curl git sqlite3 libsqlite3-dev
- curl https://storage.googleapis.com/dart-archive/channels/stable/release/2.7.2/linux_packages/dart_2.7.2-1_amd64.deb > dart.deb
- apt install -y ./dart.deb
- ln -s /usr/lib/dart/bin/pub /usr/bin/

View file

@ -48,6 +48,6 @@ export 'package:famedlysdk/src/event.dart';
export 'package:famedlysdk/src/presence.dart';
export 'package:famedlysdk/src/room.dart';
export 'package:famedlysdk/src/room_account_data.dart';
export 'package:famedlysdk/src/store_api.dart';
export 'package:famedlysdk/src/timeline.dart';
export 'package:famedlysdk/src/user.dart';
export 'package:famedlysdk/src/database/database.dart' show Database;

View file

@ -22,6 +22,7 @@
*/
import 'package:famedlysdk/famedlysdk.dart';
import './database/database.dart' show DbAccountData;
/// The global private data created by this user.
class AccountData {
@ -38,4 +39,10 @@ class AccountData {
final content = Event.getMapFromPayload(jsonPayload['content']);
return AccountData(content: content, typeKey: jsonPayload['type']);
}
/// Get account data from DbAccountData
factory AccountData.fromDb(DbAccountData dbEntry) {
final content = Event.getMapFromPayload(dbEntry.content);
return AccountData(content: content, typeKey: dbEntry.type);
}
}

View file

@ -30,7 +30,6 @@ import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/account_data.dart';
import 'package:famedlysdk/src/presence.dart';
import 'package:famedlysdk/src/room.dart';
import 'package:famedlysdk/src/store_api.dart';
import 'package:famedlysdk/src/sync/user_update.dart';
import 'package:famedlysdk/src/utils/device_keys_list.dart';
import 'package:famedlysdk/src/utils/matrix_file.dart';
@ -54,6 +53,7 @@ import 'sync/user_update.dart';
import 'user.dart';
import 'utils/matrix_exception.dart';
import 'utils/profile.dart';
import 'database/database.dart' show Database;
import 'utils/pusher.dart';
typedef RoomSorter = int Function(Room a, Room b);
@ -70,12 +70,12 @@ class Client {
@deprecated
Client get connection => this;
/// Optional persistent store for all data.
ExtendedStoreAPI get store => (storeAPI?.extended ?? false) ? storeAPI : null;
int _id;
int get id => _id;
StoreAPI storeAPI;
Database database;
Client(this.clientName, {this.debug = false, this.storeAPI}) {
Client(this.clientName, {this.debug = false, this.database}) {
onLoginStateChanged.stream.listen((loginState) {
print('LoginState: ${loginState.toString()}');
});
@ -111,10 +111,6 @@ class Client {
String get deviceName => _deviceName;
String _deviceName;
/// Which version of the matrix specification does this server support?
List<String> get matrixVersions => _matrixVersions;
List<String> _matrixVersions;
/// Returns the current login state.
bool isLogged() => accessToken != null;
@ -208,7 +204,7 @@ class Client {
/// Checks the supported versions of the Matrix protocol and the supported
/// login types. Returns false if the server is not compatible with the
/// client. Automatically sets [matrixVersions].
/// client.
/// Throws FormatException, TimeoutException and MatrixException on error.
Future<bool> checkServer(serverUrl) async {
try {
@ -226,8 +222,6 @@ class Client {
}
}
_matrixVersions = versions;
final loginResp =
await jsonRequest(type: HTTPType.GET, action: '/client/r0/login');
@ -243,7 +237,7 @@ class Client {
}
return true;
} catch (_) {
_homeserver = _matrixVersions = null;
_homeserver = null;
rethrow;
}
}
@ -292,8 +286,7 @@ class Client {
newUserID: response['user_id'],
newHomeserver: homeserver,
newDeviceName: initialDeviceDisplayName ?? '',
newDeviceID: response['device_id'],
newMatrixVersions: matrixVersions);
newDeviceID: response['device_id']);
}
return response;
}
@ -334,7 +327,6 @@ class Client {
newHomeserver: homeserver,
newDeviceName: initialDeviceDisplayName ?? '',
newDeviceID: loginResp['device_id'],
newMatrixVersions: matrixVersions,
);
return true;
}
@ -676,20 +668,39 @@ class Client {
String newUserID,
String newDeviceName,
String newDeviceID,
List<String> newMatrixVersions,
String newPrevBatch,
String newOlmAccount,
}) async {
_accessToken = newToken;
_homeserver = newHomeserver;
_userID = newUserID;
_deviceID = newDeviceID;
_deviceName = newDeviceName;
_matrixVersions = newMatrixVersions;
prevBatch = newPrevBatch;
String olmAccount;
if (database != null) {
final account = await database.getClient(clientName);
if (account != null) {
_id = account.clientId;
_homeserver = account.homeserverUrl;
_accessToken = account.token;
_userID = account.userId;
_deviceID = account.deviceId;
_deviceName = account.deviceName;
prevBatch = account.prevBatch;
olmAccount = account.olmAccount;
}
}
_accessToken = newToken ?? _accessToken;
_homeserver = newHomeserver ?? _homeserver;
_userID = newUserID ?? _userID;
_deviceID = newDeviceID ?? _deviceID;
_deviceName = newDeviceName ?? _deviceName;
prevBatch = newPrevBatch ?? prevBatch;
olmAccount = newOlmAccount ?? olmAccount;
if (_accessToken == null || _homeserver == null || _userID == null) {
// we aren't logged in
onLoginStateChanged.add(LoginState.logged);
return;
}
// Try to create a new olm account or restore a previous one.
if (newOlmAccount == null) {
if (olmAccount == null) {
try {
await olm.init();
_olmAccount = olm.Account();
@ -704,39 +715,30 @@ class Client {
try {
await olm.init();
_olmAccount = olm.Account();
_olmAccount.unpickle(userID, newOlmAccount);
_olmAccount.unpickle(userID, olmAccount);
} catch (_) {
_olmAccount = null;
}
}
if (storeAPI != null) {
await storeAPI.storeClient();
_userDeviceKeys = await storeAPI.getUserDeviceKeys();
final String olmSessionPickleString =
await storeAPI.getItem('/clients/$userID/olm-sessions');
if (olmSessionPickleString != null) {
final Map<String, dynamic> pickleMap =
json.decode(olmSessionPickleString);
for (var entry in pickleMap.entries) {
for (String pickle in entry.value) {
_olmSessions[entry.key] = [];
try {
var session = olm.Session();
session.unpickle(userID, pickle);
_olmSessions[entry.key].add(session);
} catch (e) {
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
}
}
}
}
if (store != null) {
_rooms = await store.getRoomList(onlyLeft: false);
_sortRooms();
accountData = await store.getAccountData();
presences = await store.getPresences();
if (database != null) {
if (id != null) {
await database.updateClient(
_homeserver, _accessToken, _userID, _deviceID,
_deviceName, prevBatch, pickledOlmAccount, id,
);
} else {
_id = await database.insertClient(
clientName, _homeserver, _accessToken, _userID, _deviceID,
_deviceName, prevBatch, pickledOlmAccount,
);
}
_userDeviceKeys = await database.getUserDeviceKeys(id);
_olmSessions = await database.getOlmSessions(id, _userID);
_rooms = await database.getRoomList(this, onlyLeft: false);
_sortRooms();
accountData = await database.getAccountData(id);
presences = await database.getPresences(id);
}
_userEventSub ??= onUserEvent.stream.listen(handleUserUpdate);
@ -755,14 +757,14 @@ class Client {
});
rooms.forEach((Room room) {
room.clearOutboundGroupSession(wipe: true);
room.sessionKeys.values.forEach((SessionKey sessionKey) {
room.inboundGroupSessions.values.forEach((SessionKey sessionKey) {
sessionKey.inboundGroupSession?.free();
});
});
_olmAccount?.free();
storeAPI?.clear();
_accessToken = _homeserver =
_userID = _deviceID = _deviceName = _matrixVersions = prevBatch = null;
database?.clear(id);
_id = _accessToken = _homeserver =
_userID = _deviceID = _deviceName = prevBatch = null;
_rooms = [];
onLoginStateChanged.add(LoginState.loggedOut);
}
@ -858,6 +860,7 @@ class Client {
var exception = MatrixException(resp);
if (exception.error == MatrixError.M_UNKNOWN_TOKEN) {
// The token is no longer valid. Need to sign off....
// TODO: add a way to export keys prior logout?
onError.add(exception);
clear();
}
@ -926,14 +929,15 @@ class Client {
final syncResp = await _syncRequest;
if (hash != _syncRequest.hashCode) return;
_timeoutFactor = 1;
if (store != null) {
await store.transaction(() {
handleSync(syncResp);
store.storePrevBatch(syncResp['next_batch']);
});
} else {
await handleSync(syncResp);
}
final futures = handleSync(syncResp);
await database?.transaction(() async {
for (final f in futures) {
await f();
}
if (prevBatch != syncResp['next_batch']) {
await database.storePrevBatch(syncResp['next_batch'], id);
}
});
if (prevBatch == null) {
onFirstSync.add(true);
prevBatch = syncResp['next_batch'];
@ -946,37 +950,39 @@ class Client {
onError.add(exception);
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
} catch (exception) {
print('Error during processing events: ' + exception.toString());
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
}
}
/// Use this method only for testing utilities!
void handleSync(dynamic sync) {
List<Future<dynamic> Function()> handleSync(dynamic sync) {
final dbActions = <Future<dynamic> Function()>[];
if (sync['to_device'] is Map<String, dynamic> &&
sync['to_device']['events'] is List<dynamic>) {
_handleToDeviceEvents(sync['to_device']['events']);
}
if (sync['rooms'] is Map<String, dynamic>) {
if (sync['rooms']['join'] is Map<String, dynamic>) {
_handleRooms(sync['rooms']['join'], Membership.join);
_handleRooms(sync['rooms']['join'], Membership.join, dbActions);
}
if (sync['rooms']['invite'] is Map<String, dynamic>) {
_handleRooms(sync['rooms']['invite'], Membership.invite);
_handleRooms(sync['rooms']['invite'], Membership.invite, dbActions);
}
if (sync['rooms']['leave'] is Map<String, dynamic>) {
_handleRooms(sync['rooms']['leave'], Membership.leave);
_handleRooms(sync['rooms']['leave'], Membership.leave, dbActions);
}
}
if (sync['presence'] is Map<String, dynamic> &&
sync['presence']['events'] is List<dynamic>) {
_handleGlobalEvents(sync['presence']['events'], 'presence');
_handleGlobalEvents(sync['presence']['events'], 'presence', dbActions);
}
if (sync['account_data'] is Map<String, dynamic> &&
sync['account_data']['events'] is List<dynamic>) {
_handleGlobalEvents(sync['account_data']['events'], 'account_data');
_handleGlobalEvents(sync['account_data']['events'], 'account_data', dbActions);
}
if (sync['device_lists'] is Map<String, dynamic>) {
_handleDeviceListsEvents(sync['device_lists']);
_handleDeviceListsEvents(sync['device_lists'], dbActions);
}
if (sync['device_one_time_keys_count'] is Map<String, dynamic>) {
_handleDeviceOneTimeKeysCount(sync['device_one_time_keys_count']);
@ -988,6 +994,7 @@ class Client {
);
}
onSync.add(sync);
return dbActions;
}
void _handleDeviceOneTimeKeysCount(
@ -1004,11 +1011,14 @@ class Client {
}
}
void _handleDeviceListsEvents(Map<String, dynamic> deviceLists) {
void _handleDeviceListsEvents(Map<String, dynamic> deviceLists, List<Future<dynamic> Function()> dbActions) {
if (deviceLists['changed'] is List) {
for (final userId in deviceLists['changed']) {
if (_userDeviceKeys.containsKey(userId)) {
_userDeviceKeys[userId].outdated = true;
if (database != null) {
dbActions.add(() => database.storeUserDeviceKeysInfo(id, userId, true));
}
}
}
for (final userId in deviceLists['left']) {
@ -1046,8 +1056,8 @@ class Client {
}
}
void _handleRooms(Map<String, dynamic> rooms, Membership membership) {
rooms.forEach((String id, dynamic room) async {
void _handleRooms(Map<String, dynamic> rooms, Membership membership, List<Future<dynamic> Function()> dbActions) {
rooms.forEach((String id, dynamic room) {
// calculate the notification counts, the limitedTimeline and prevbatch
num highlight_count = 0;
num notification_count = 0;
@ -1089,40 +1099,55 @@ class Client {
summary: summary,
);
_updateRoomsByRoomUpdate(update);
unawaited(store?.storeRoomUpdate(update));
final roomObj = getRoomById(id);
if (limitedTimeline && roomObj != null) {
roomObj.resetSortOrder();
}
if (database != null) {
dbActions.add(() => database.storeRoomUpdate(this.id, update, getRoomById(id)));
}
onRoomUpdate.add(update);
var handledEvents = false;
/// Handle now all room events and save them in the database
if (room['state'] is Map<String, dynamic> &&
room['state']['events'] is List<dynamic>) {
_handleRoomEvents(id, room['state']['events'], 'state');
room['state']['events'] is List<dynamic> &&
room['state']['events'].isNotEmpty) {
_handleRoomEvents(id, room['state']['events'], 'state', dbActions);
handledEvents = true;
}
if (room['invite_state'] is Map<String, dynamic> &&
room['invite_state']['events'] is List<dynamic>) {
_handleRoomEvents(id, room['invite_state']['events'], 'invite_state');
_handleRoomEvents(id, room['invite_state']['events'], 'invite_state', dbActions);
}
if (room['timeline'] is Map<String, dynamic> &&
room['timeline']['events'] is List<dynamic>) {
_handleRoomEvents(id, room['timeline']['events'], 'timeline');
room['timeline']['events'] is List<dynamic> &&
room['timeline']['events'].isNotEmpty) {
_handleRoomEvents(id, room['timeline']['events'], 'timeline', dbActions);
handledEvents = true;
}
if (room['ephemeral'] is Map<String, dynamic> &&
room['ephemeral']['events'] is List<dynamic>) {
_handleEphemerals(id, room['ephemeral']['events']);
_handleEphemerals(id, room['ephemeral']['events'], dbActions);
}
if (room['account_data'] is Map<String, dynamic> &&
room['account_data']['events'] is List<dynamic>) {
_handleRoomEvents(id, room['account_data']['events'], 'account_data');
_handleRoomEvents(id, room['account_data']['events'], 'account_data', dbActions);
}
if (handledEvents && database != null && roomObj != null) {
dbActions.add(() => roomObj.updateSortOrder());
}
});
}
void _handleEphemerals(String id, List<dynamic> events) {
void _handleEphemerals(String id, List<dynamic> events, List<Future<dynamic> Function()> dbActions) {
for (num i = 0; i < events.length; i++) {
_handleEvent(events[i], id, 'ephemeral');
_handleEvent(events[i], id, 'ephemeral', dbActions);
// Receipt events are deltas between two states. We will create a
// fake room account data event for this and store the difference
@ -1142,12 +1167,12 @@ class Client {
final mxid = userTimestampMapEntry.key;
// Remove previous receipt event from this user
for (var entry in receiptStateContent.entries) {
if (entry.value['m.read'] is Map<String, dynamic> &&
entry.value['m.read'].containsKey(mxid)) {
entry.value['m.read'].remove(mxid);
break;
}
if (
receiptStateContent[eventID] is Map<String, dynamic> &&
receiptStateContent[eventID]['m.read'] is Map<String, dynamic> &&
receiptStateContent[eventID]['m.read'].containsKey(mxid)
) {
receiptStateContent[eventID]['m.read'].remove(mxid);
}
if (userTimestampMap[mxid] is Map<String, dynamic> &&
userTimestampMap[mxid].containsKey('ts')) {
@ -1160,18 +1185,18 @@ class Client {
}
}
events[i]['content'] = receiptStateContent;
_handleEvent(events[i], id, 'account_data');
_handleEvent(events[i], id, 'account_data', dbActions);
}
}
}
void _handleRoomEvents(String chat_id, List<dynamic> events, String type) {
void _handleRoomEvents(String chat_id, List<dynamic> events, String type, List<Future<dynamic> Function()> dbActions) {
for (num i = 0; i < events.length; i++) {
_handleEvent(events[i], chat_id, type);
_handleEvent(events[i], chat_id, type, dbActions);
}
}
void _handleGlobalEvents(List<dynamic> events, String type) {
void _handleGlobalEvents(List<dynamic> events, String type, List<Future<dynamic> Function()> dbActions) {
for (var i = 0; i < events.length; i++) {
if (events[i]['type'] is String &&
events[i]['content'] is Map<String, dynamic>) {
@ -1180,42 +1205,51 @@ class Client {
type: type,
content: events[i],
);
store?.storeUserEventUpdate(update);
if (database != null) {
dbActions.add(() => database.storeUserEventUpdate(id, update));
}
onUserEvent.add(update);
}
}
}
void _handleEvent(Map<String, dynamic> event, String roomID, String type) {
void _handleEvent(Map<String, dynamic> event, String roomID, String type, List<Future<dynamic> Function()> dbActions) {
if (event['type'] is String && event['content'] is Map<String, dynamic>) {
// The client must ignore any new m.room.encryption event to prevent
// man-in-the-middle attacks!
if (event['type'] == 'm.room.encryption' &&
getRoomById(roomID).encrypted) {
final room = getRoomById(roomID);
if (room == null || (event['type'] == 'm.room.encryption' &&
room.encrypted)) {
return;
}
// ephemeral events aren't persisted and don't need a sort order - they are
// expected to be processed as soon as they come in
final sortOrder = type != 'ephemeral' ? room.newSortOrder : 0.0;
var update = EventUpdate(
eventType: event['type'],
roomID: roomID,
type: type,
content: event,
sortOrder: sortOrder,
);
if (event['type'] == 'm.room.encrypted') {
update = update.decrypt(getRoomById(update.roomID));
update = update.decrypt(room);
}
if (type != 'ephemeral' && database != null) {
dbActions.add(() => database.storeEventUpdate(id, update));
}
store?.storeEventUpdate(update);
_updateRoomsByEventUpdate(update);
onEvent.add(update);
if (event['type'] == 'm.call.invite') {
onCallInvite.add(Event.fromJson(event, getRoomById(roomID)));
onCallInvite.add(Event.fromJson(event, room, sortOrder));
} else if (event['type'] == 'm.call.hangup') {
onCallHangup.add(Event.fromJson(event, getRoomById(roomID)));
onCallHangup.add(Event.fromJson(event, room, sortOrder));
} else if (event['type'] == 'm.call.answer') {
onCallAnswer.add(Event.fromJson(event, getRoomById(roomID)));
onCallAnswer.add(Event.fromJson(event, room, sortOrder));
} else if (event['type'] == 'm.call.candidates') {
onCallCandidates.add(Event.fromJson(event, getRoomById(roomID)));
onCallCandidates.add(Event.fromJson(event, room, sortOrder));
}
}
}
@ -1294,7 +1328,7 @@ class Client {
if (eventUpdate.type == 'timeline' ||
eventUpdate.type == 'state' ||
eventUpdate.type == 'invite_state') {
var stateEvent = Event.fromJson(eventUpdate.content, rooms[j]);
var stateEvent = Event.fromJson(eventUpdate.content, rooms[j], eventUpdate.sortOrder);
if (stateEvent.type == EventTypes.Redaction) {
final String redacts = eventUpdate.content['redacts'];
rooms[j].states.states.forEach(
@ -1337,6 +1371,7 @@ class Client {
var room = getRoomById(roomId);
if (room == null && addToPendingIfNotFound) {
_pendingToDeviceEvents.add(toDeviceEvent);
break;
}
final String sessionId = toDeviceEvent.content['session_id'];
if (toDeviceEvent.type == 'm.room_key' &&
@ -1349,7 +1384,7 @@ class Client {
.deviceKeys[toDeviceEvent.content['requesting_device_id']]
.ed25519Key;
}
room.setSessionKey(
room.setInboundGroupSession(
sessionId,
toDeviceEvent.content,
forwarded: toDeviceEvent.type == 'm.forwarded_room_key',
@ -1379,7 +1414,7 @@ class Client {
.containsKey(toDeviceEvent.content['requesting_device_id'])) {
deviceKeys = userDeviceKeys[toDeviceEvent.sender]
.deviceKeys[toDeviceEvent.content['requesting_device_id']];
if (room.sessionKeys.containsKey(sessionId)) {
if (room.inboundGroupSessions.containsKey(sessionId)) {
final roomKeyRequest =
RoomKeyRequest.fromToDeviceEvent(toDeviceEvent, this);
if (deviceKeys.userId == userID &&
@ -1446,6 +1481,7 @@ class Client {
Future<void> _updateUserDeviceKeys() async {
try {
if (!isLogged()) return;
final dbActions = <Future<dynamic> Function()>[];
var trackedUserIds = await _getUserIdsInEncryptedRooms();
trackedUserIds.add(userID);
@ -1481,6 +1517,7 @@ class Client {
final String deviceId = rawDeviceKeyEntry.key;
// Set the new device key for this device
if (!oldKeys.containsKey(deviceId)) {
_userDeviceKeys[userId].deviceKeys[deviceId] =
DeviceKeys.fromJson(rawDeviceKeyEntry.value);
@ -1493,11 +1530,34 @@ class Client {
} else {
_userDeviceKeys[userId].deviceKeys[deviceId] = oldKeys[deviceId];
}
if (database != null) {
dbActions.add(() => database.storeUserDeviceKey(id, userId, deviceId,
json.encode(_userDeviceKeys[userId].deviceKeys[deviceId].toJson()),
_userDeviceKeys[userId].deviceKeys[deviceId].verified,
_userDeviceKeys[userId].deviceKeys[deviceId].blocked,
));
}
}
if (database != null) {
for (final oldDeviceKeyEntry in oldKeys.entries) {
final deviceId = oldDeviceKeyEntry.key;
if (!_userDeviceKeys[userId].deviceKeys.containsKey(deviceId)) {
// we need to remove an old key
dbActions.add(() => database.removeUserDeviceKey(id, userId, deviceId));
}
}
}
_userDeviceKeys[userId].outdated = false;
if (database != null) {
dbActions.add(() => database.storeUserDeviceKeysInfo(id, userId, false));
}
}
}
await storeAPI?.storeUserDeviceKeys(userDeviceKeys);
await database?.transaction(() async {
for (final f in dbActions) {
await f();
}
});
rooms.forEach((Room room) {
if (room.encrypted) {
room.clearOutboundGroupSession();
@ -1616,7 +1676,7 @@ class Client {
oneTimeKeysCount) {
return false;
}
await storeAPI?.storeClient();
await database?.updateClientKeys(pickledOlmAccount, id);
lastTimeKeysUploaded = DateTime.now();
return true;
}
@ -1668,7 +1728,7 @@ class Client {
var newSession = olm.Session();
newSession.create_inbound_from(_olmAccount, senderKey, body);
_olmAccount.remove_one_time_keys(newSession);
storeAPI?.storeClient();
database?.updateClientKeys(pickledOlmAccount, id);
plaintext = newSession.decrypt(type, body);
storeOlmSession(senderKey, newSession);
}
@ -1695,29 +1755,24 @@ class Client {
/// A map from Curve25519 identity keys to existing olm sessions.
Map<String, List<olm.Session>> get olmSessions => _olmSessions;
final Map<String, List<olm.Session>> _olmSessions = {};
Map<String, List<olm.Session>> _olmSessions = {};
void storeOlmSession(String curve25519IdentityKey, olm.Session session) {
if (!_olmSessions.containsKey(curve25519IdentityKey)) {
_olmSessions[curve25519IdentityKey] = [];
}
if (_olmSessions[curve25519IdentityKey]
.indexWhere((s) => s.session_id() == session.session_id()) ==
final ix = _olmSessions[curve25519IdentityKey]
.indexWhere((s) => s.session_id() == session.session_id());
if (ix ==
-1) {
// add a new session
_olmSessions[curve25519IdentityKey].add(session);
} else {
// update an existing session
_olmSessions[curve25519IdentityKey][ix] = session;
}
var pickleMap = <String, List<String>>{};
for (var entry in olmSessions.entries) {
pickleMap[entry.key] = [];
for (var session in entry.value) {
try {
pickleMap[entry.key].add(session.pickle(userID));
} catch (e) {
print('[LibOlm] Could not pickle olm session: ' + e.toString());
}
}
}
storeAPI?.setItem('/clients/$userID/olm-sessions', json.encode(pickleMap));
final pickle = session.pickle(userID);
database?.storeOlmSession(id, curve25519IdentityKey, session.session_id(), pickle);
}
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send

View file

@ -0,0 +1,368 @@
import 'package:moor/moor.dart';
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart' as sdk;
import 'package:olm/olm.dart' as olm;
part 'database.g.dart';
@UseMoor(
include: {'database.moor'},
)
class Database extends _$Database {
Database(QueryExecutor e) : super(e);
@override
int get schemaVersion => 2;
int get maxFileSize => 1 * 1024 * 1024;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) {
return m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
// this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here
if (from == 1) {
await m.createIndex(userDeviceKeysIndex);
await m.createIndex(userDeviceKeysKeyIndex);
await m.createIndex(olmSessionsIndex);
await m.createIndex(outboundGroupSessionsIndex);
await m.createIndex(inboundGroupSessionsIndex);
await m.createIndex(roomsIndex);
await m.createIndex(eventsIndex);
await m.createIndex(roomStatesIndex);
await m.createIndex(accountDataIndex);
await m.createIndex(roomAccountDataIndex);
await m.createIndex(presencesIndex);
}
},
);
Future<DbClient> getClient(String name) async {
final res = await dbGetClient(name).get();
if (res.isEmpty) return null;
return res.first;
}
Future<Map<String, sdk.DeviceKeysList>> getUserDeviceKeys(int clientId) async {
final deviceKeys = await getAllUserDeviceKeys(clientId).get();
if (deviceKeys.isEmpty) {
return {};
}
final deviceKeysKeys = await getAllUserDeviceKeysKeys(clientId).get();
final res = <String, sdk.DeviceKeysList>{};
for (final entry in deviceKeys) {
res[entry.userId] = sdk.DeviceKeysList.fromDb(entry, deviceKeysKeys.where((k) => k.userId == entry.userId).toList());
}
return res;
}
Future<Map<String, List<olm.Session>>> getOlmSessions(int clientId, String userId) async {
final raw = await getAllOlmSessions(clientId).get();
if (raw.isEmpty) {
return {};
}
final res = <String, List<olm.Session>>{};
for (final row in raw) {
if (!res.containsKey(row.identityKey)) {
res[row.identityKey] = [];
}
try {
var session = olm.Session();
session.unpickle(userId, row.pickle);
res[row.identityKey].add(session);
} catch (e) {
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
}
}
return res;
}
Future<DbOutboundGroupSession> getDbOutboundGroupSession(int clientId, String roomId) async {
final res = await dbGetOutboundGroupSession(clientId, roomId).get();
if (res.isEmpty) {
return null;
}
return res.first;
}
Future<List<DbInboundGroupSession>> getDbInboundGroupSessions(int clientId, String roomId) async {
return await dbGetInboundGroupSessionKeys(clientId, roomId).get();
}
Future<List<sdk.Room>> getRoomList(sdk.Client client, {bool onlyLeft = false}) async {
final res = await (select(rooms)..where((t) => onlyLeft
? t.membership.equals('leave')
: t.membership.equals('leave').not())).get();
final resStates = await getAllRoomStates(client.id).get();
final resAccountData = await getAllRoomAccountData(client.id).get();
final resOutboundGroupSessions = await getAllOutboundGroupSessions(client.id).get();
final resInboundGroupSessions = await getAllInboundGroupSessions(client.id).get();
final roomList = <sdk.Room>[];
for (final r in res) {
final outboundGroupSession = resOutboundGroupSessions.where((rs) => rs.roomId == r.roomId);
final room = await sdk.Room.getRoomFromTableRow(
r,
client,
states: resStates.where((rs) => rs.roomId == r.roomId),
roomAccountData: resAccountData.where((rs) => rs.roomId == r.roomId),
outboundGroupSession: outboundGroupSession.isEmpty ? false : outboundGroupSession.first,
inboundGroupSessions: resInboundGroupSessions.where((rs) => rs.roomId == r.roomId),
);
roomList.add(room);
}
return roomList;
}
Future<Map<String, sdk.AccountData>> getAccountData(int clientId) async {
final newAccountData = <String, sdk.AccountData>{};
final rawAccountData = await getAllAccountData(clientId).get();
for (final d in rawAccountData) {
newAccountData[d.type] = sdk.AccountData.fromDb(d);
}
return newAccountData;
}
Future<Map<String, sdk.Presence>> getPresences(int clientId) async {
final newPresences = <String, sdk.Presence>{};
final rawPresences = await getAllPresences(clientId).get();
for (final d in rawPresences) {
newPresences[d.sender] = sdk.Presence.fromDb(d);
}
return newPresences;
}
/// Stores a RoomUpdate object in the database. Must be called inside of
/// [transaction].
final Set<String> _ensuredRooms = {};
Future<void> storeRoomUpdate(int clientId, sdk.RoomUpdate roomUpdate, [sdk.Room oldRoom]) async {
final setKey = '${clientId};${roomUpdate.id}';
if (roomUpdate.membership != sdk.Membership.leave) {
if (!_ensuredRooms.contains(setKey)) {
await ensureRoomExists(clientId, roomUpdate.id, roomUpdate.membership.toString().split('.').last);
_ensuredRooms.add(setKey);
}
} else {
_ensuredRooms.remove(setKey);
await removeRoom(clientId, roomUpdate.id);
return;
}
var doUpdate = oldRoom == null;
if (!doUpdate) {
doUpdate = roomUpdate.highlight_count != oldRoom.highlightCount ||
roomUpdate.notification_count != oldRoom.notificationCount ||
roomUpdate.membership.toString().split('.').last != oldRoom.membership.toString().split('.').last ||
(roomUpdate.summary?.mJoinedMemberCount != null &&
roomUpdate.summary.mJoinedMemberCount != oldRoom.mInvitedMemberCount) ||
(roomUpdate.summary?.mInvitedMemberCount != null &&
roomUpdate.summary.mJoinedMemberCount != oldRoom.mJoinedMemberCount) ||
(roomUpdate.summary?.mHeroes != null &&
roomUpdate.summary.mHeroes.join(',') != oldRoom.mHeroes.join(','));
}
if (doUpdate) {
await (update(rooms)..where((r) => r.roomId.equals(roomUpdate.id) & r.clientId.equals(clientId))).write(RoomsCompanion(
highlightCount: Value(roomUpdate.highlight_count),
notificationCount: Value(roomUpdate.notification_count),
membership: Value(roomUpdate.membership.toString().split('.').last),
joinedMemberCount: roomUpdate.summary?.mJoinedMemberCount != null ? Value(roomUpdate.summary.mJoinedMemberCount) : Value.absent(),
invitedMemberCount: roomUpdate.summary?.mInvitedMemberCount != null ? Value(roomUpdate.summary.mInvitedMemberCount) : Value.absent(),
heroes: roomUpdate.summary?.mHeroes != null ? Value(roomUpdate.summary.mHeroes.join(',')) : Value.absent(),
));
}
// Is the timeline limited? Then all previous messages should be
// removed from the database!
if (roomUpdate.limitedTimeline) {
await removeRoomEvents(clientId, roomUpdate.id);
await updateRoomSortOrder(0.0, 0.0, clientId, roomUpdate.id);
await setRoomPrevBatch(roomUpdate.prev_batch, clientId, roomUpdate.id);
}
}
/// Stores an UserUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeUserEventUpdate(int clientId, sdk.UserUpdate userUpdate) async {
if (userUpdate.type == 'account_data') {
await storeAccountData(clientId, userUpdate.eventType, json.encode(userUpdate.content['content']));
} else if (userUpdate.type == 'presence') {
await storePresence(clientId, userUpdate.eventType, userUpdate.content['sender'], json.encode(userUpdate.content['content']));
}
}
/// Stores an EventUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeEventUpdate(int clientId, sdk.EventUpdate eventUpdate) async {
if (eventUpdate.type == 'ephemeral') return;
final eventContent = eventUpdate.content;
final type = eventUpdate.type;
final chatId = eventUpdate.roomID;
// Get the state_key for state events
var stateKey = '';
if (eventContent['state_key'] is String) {
stateKey = eventContent['state_key'];
}
if (eventUpdate.eventType == 'm.room.redaction') {
await redactMessage(clientId, eventUpdate);
}
if (type == 'timeline' || type == 'history') {
// calculate the status
var status = 2;
if (eventContent['status'] is num) status = eventContent['status'];
if ((status == 1 || status == -1) &&
eventContent['unsigned'] is Map<String, dynamic> &&
eventContent['unsigned']['transaction_id'] is String) {
// status changed and we have an old transaction id --> update event id and stuffs
await updateEventStatus(status, eventContent['event_id'], clientId, eventContent['unsigned']['transaction_id'], chatId);
} else {
DbEvent oldEvent;
if (type == 'history') {
final allOldEvents = await getEvent(clientId, eventContent['event_id'], chatId).get();
if (allOldEvents.isNotEmpty) {
oldEvent = allOldEvents.first;
}
}
await storeEvent(
clientId,
eventContent['event_id'],
chatId,
oldEvent?.sortOrder ?? eventUpdate.sortOrder,
eventContent['origin_server_ts'] != null ? DateTime.fromMillisecondsSinceEpoch(eventContent['origin_server_ts']) : DateTime.now(),
eventContent['sender'],
eventContent['type'],
json.encode(eventContent['unsigned'] ?? ''),
json.encode(eventContent['content']),
json.encode(eventContent['prevContent']),
eventContent['state_key'],
status,
);
}
// is there a transaction id? Then delete the event with this id.
if (status != -1 &&
eventUpdate.content.containsKey('unsigned') &&
eventUpdate.content['unsigned']['transaction_id'] is String) {
await removeEvent(clientId, eventUpdate.content['unsigned']['transaction_id'], chatId);
}
}
if (type == 'history') return;
if (type != 'account_data') {
final now = DateTime.now();
await storeRoomState(
clientId,
eventContent['event_id'] ?? now.millisecondsSinceEpoch.toString(),
chatId,
eventUpdate.sortOrder ?? 0.0,
eventContent['origin_server_ts'] != null ? DateTime.fromMillisecondsSinceEpoch(eventContent['origin_server_ts']) : now,
eventContent['sender'],
eventContent['type'],
json.encode(eventContent['unsigned'] ?? ''),
json.encode(eventContent['content']),
json.encode(eventContent['prev_content'] ?? ''),
stateKey,
);
} else if (type == 'account_data') {
await storeRoomAccountData(
clientId,
eventContent['type'],
chatId,
json.encode(eventContent['content']),
);
}
}
Future<sdk.Event> getEventById(int clientId, String eventId, sdk.Room room) async {
final event = await getEvent(clientId, eventId, room.id).get();
if (event.isEmpty) {
return null;
}
return sdk.Event.fromDb(event.first, room);
}
Future<bool> redactMessage(int clientId, sdk.EventUpdate eventUpdate) async {
final events = await getEvent(clientId, eventUpdate.content['redacts'], eventUpdate.roomID).get();
var success = false;
for (final dbEvent in events) {
final event = sdk.Event.fromDb(dbEvent, null);
event.setRedactionEvent(sdk.Event.fromJson(eventUpdate.content, null));
final changes1 = await updateEvent(
json.encode(event.unsigned ?? ''),
json.encode(event.content ?? ''),
json.encode(event.prevContent ?? ''),
clientId,
event.eventId,
eventUpdate.roomID,
);
final changes2 = await updateEvent(
json.encode(event.unsigned ?? ''),
json.encode(event.content ?? ''),
json.encode(event.prevContent ?? ''),
clientId,
event.eventId,
eventUpdate.roomID,
);
if (changes1 == 1 && changes2 == 1) success = true;
}
return success;
}
Future<void> forgetRoom(int clientId, String roomId) async {
final setKey = '${clientId};${roomId}';
_ensuredRooms.remove(setKey);
await (delete(rooms)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
await (delete(events)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
await (delete(roomStates)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
await (delete(roomAccountData)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
}
Future<void> clearCache(int clientId) async {
await (delete(presences)..where((r) => r.clientId.equals(clientId))).go();
await (delete(roomAccountData)..where((r) => r.clientId.equals(clientId))).go();
await (delete(accountData)..where((r) => r.clientId.equals(clientId))).go();
await (delete(roomStates)..where((r) => r.clientId.equals(clientId))).go();
await (delete(events)..where((r) => r.clientId.equals(clientId))).go();
await (delete(rooms)..where((r) => r.clientId.equals(clientId))).go();
await (delete(outboundGroupSessions)..where((r) => r.clientId.equals(clientId))).go();
await storePrevBatch(null, clientId);
}
Future<void> clear(int clientId) async {
await clearCache(clientId);
await (delete(inboundGroupSessions)..where((r) => r.clientId.equals(clientId))).go();
await (delete(olmSessions)..where((r) => r.clientId.equals(clientId))).go();
await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId))).go();
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId))).go();
await (delete(clients)..where((r) => r.clientId.equals(clientId))).go();
}
Future<sdk.User> getUser(int clientId, String userId, sdk.Room room) async {
final res = await dbGetUser(clientId, userId, room.id).get();
if (res.isEmpty) {
return null;
}
return sdk.Event.fromDb(res.first, room).asUser;
}
Future<List<sdk.Event>> getEventList(int clientId, sdk.Room room) async {
final res = await dbGetEventList(clientId, room.id).get();
final eventList = <sdk.Event>[];
for (final r in res) {
eventList.add(sdk.Event.fromDb(r, room));
}
return eventList;
}
Future<Uint8List> getFile(String mxcUri) async {
final res = await dbGetFile(mxcUri).get();
if (res.isEmpty) return null;
return res.first.bytes;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,193 @@
-- Table definitions
CREATE TABLE clients (
client_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
homeserver_url TEXT NOT NULL,
token TEXT NOT NULL,
user_id TEXT NOT NULL,
device_id TEXT,
device_name TEXT,
prev_batch TEXT,
olm_account TEXT,
UNIQUE(name)
) AS DbClient;
CREATE TABLE user_device_keys (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
user_id TEXT NOT NULL,
outdated BOOLEAN DEFAULT true,
UNIQUE(client_id, user_id)
) as DbUserDeviceKey;
CREATE INDEX user_device_keys_index ON user_device_keys(client_id);
CREATE TABLE user_device_keys_key (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
user_id TEXT NOT NULL,
device_id TEXT NOT NULL,
content TEXT NOT NULL,
verified BOOLEAN DEFAULT false,
blocked BOOLEAN DEFAULT false,
UNIQUE(client_id, user_id, device_id)
) as DbUserDeviceKeysKey;
CREATE INDEX user_device_keys_key_index ON user_device_keys_key(client_id);
CREATE TABLE olm_sessions (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
identity_key TEXT NOT NULL,
session_id TEXT NOT NULL,
pickle TEXT NOT NULL,
UNIQUE(client_id, identity_key, session_id)
) AS DbOlmSessions;
CREATE INDEX olm_sessions_index ON olm_sessions(client_id);
CREATE TABLE outbound_group_sessions (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
room_id TEXT NOT NULL,
pickle TEXT NOT NULL,
device_ids TEXT NOT NULL,
UNIQUE(client_id, room_id)
) AS DbOutboundGroupSession;
CREATE INDEX outbound_group_sessions_index ON outbound_group_sessions(client_id);
CREATE TABLE inbound_group_sessions (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
room_id TEXT NOT NULL,
session_id TEXT NOT NULL,
pickle TEXT NOT NULL,
content TEXT,
indexes TEXT,
UNIQUE(client_id, room_id, session_id)
) AS DbInboundGroupSession;
CREATE INDEX inbound_group_sessions_index ON inbound_group_sessions(client_id);
CREATE TABLE rooms (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
room_id TEXT NOT NULL,
membership TEXT NOT NULL,
highlight_count INTEGER NOT NULL DEFAULT '0',
notification_count INTEGER NOT NULL DEFAULT '0',
prev_batch TEXT DEFAULT '',
joined_member_count INTEGER NOT NULL DEFAULT '0',
invited_member_count INTEGER NOT NULL DEFAULT '0',
newest_sort_order DOUBLE NOT NULL DEFAULT '0',
oldest_sort_order DOUBLE NOT NULL DEFAULT '0',
heroes TEXT DEFAULT '',
UNIQUE(client_id, room_id)
) AS DbRoom;
CREATE INDEX rooms_index ON rooms(client_id);
CREATE TABLE events (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
event_id TEXT NOT NULL,
room_id TEXT NOT NULL,
sort_order DOUBLE NOT NULL,
origin_server_ts DATETIME NOT NULL,
sender TEXT NOT NULL,
type TEXT NOT NULL,
unsigned TEXT,
content TEXT,
prev_content TEXT,
state_key TEXT,
status INTEGER,
UNIQUE(client_id, event_id, room_id)
) AS DbEvent;
CREATE INDEX events_index ON events(client_id, room_id);
CREATE TABLE room_states (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
event_id TEXT NOT NULL,
room_id TEXT NOT NULL,
sort_order DOUBLE NOT NULL,
origin_server_ts DATETIME NOT NULL,
sender TEXT NOT NULL,
type TEXT NOT NULL,
unsigned TEXT,
content TEXT,
prev_content TEXT,
state_key TEXT NOT NULL,
UNIQUE(client_id, event_id, room_id),
UNIQUE(client_id, room_id, state_key, type)
) AS DbRoomState;
CREATE INDEX room_states_index ON room_states(client_id);
CREATE TABLE account_data (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
type TEXT NOT NULL,
content TEXT,
UNIQUE(client_id, type)
) AS DbAccountData;
CREATE INDEX account_data_index ON account_data(client_id);
CREATE TABLE room_account_data (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
type TEXT NOT NULL,
room_id TEXT NOT NULL,
content TEXT,
UNIQUE(client_id, type, room_id)
) AS DbRoomAccountData;
CREATE INDEX room_account_data_index ON room_account_data(client_id);
CREATE TABLE presences (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
type TEXT NOT NULL,
sender TEXT NOT NULL,
content TEXT,
UNIQUE(client_id, type, sender)
) AS DbPresence;
CREATE INDEX presences_index ON presences(client_id);
CREATE TABLE files (
mxc_uri TEXT NOT NULL PRIMARY KEY,
bytes BLOB,
saved_at DATETIME,
UNIQUE(mxc_uri)
) AS DbFile;
-- named queries
dbGetClient: SELECT * FROM clients WHERE name = :name;
updateClient: UPDATE clients SET homeserver_url = :homeserver_url, token = :token, user_id = :user_id, device_id = :device_id, device_name = :device_name, prev_batch = :prev_batch, olm_account = :olm_account WHERE client_id = :client_id;
updateClientKeys: UPDATE clients SET olm_account = :olm_account WHERE client_id = :client_id;
storePrevBatch: UPDATE clients SET prev_batch = :prev_batch WHERE client_id = :client_id;
getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id;
getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key WHERE client_id = :client_id;
getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id;
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle);
getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id;
dbGetOutboundGroupSession: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
storeOutboundGroupSession: INSERT OR REPLACE INTO outbound_group_sessions (client_id, room_id, pickle, device_ids) VALUES (:client_id, :room_id, :pickle, :device_ids);
removeOutboundGroupSession: DELETE FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
dbGetInboundGroupSessionKeys: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
getAllInboundGroupSessions: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id;
storeInboundGroupSession: INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes);
storeUserDeviceKeysInfo: INSERT OR REPLACE INTO user_device_keys (client_id, user_id, outdated) VALUES (:client_id, :user_id, :outdated);
setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
storeUserDeviceKey: INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :verified, :blocked);
removeUserDeviceKey: DELETE FROM user_device_keys_key WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account);
ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership);
setRoomPrevBatch: UPDATE rooms SET prev_batch = :prev_batch WHERE client_id = :client_id AND room_id = :room_id;
updateRoomSortOrder: UPDATE rooms SET oldest_sort_order = :oldest_sort_order, newest_sort_order = :newest_sort_order WHERE client_id = :client_id AND room_id = :room_id;
getAllAccountData: SELECT * FROM account_data WHERE client_id = :client_id;
storeAccountData: INSERT OR REPLACE INTO account_data (client_id, type, content) VALUES (:client_id, :type, :content);
getAllPresences: SELECT * FROM presences WHERE client_id = :client_id;
storePresence: INSERT OR REPLACE INTO presences (client_id, type, sender, content) VALUES (:client_id, :type, :sender, :content);
updateEvent: UPDATE events SET unsigned = :unsigned, content = :content, prev_content = :prev_content WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
updateEventStatus: UPDATE events SET status = :status, event_id = :new_event_id WHERE client_id = :client_id AND event_id = :old_event_id AND room_id = :room_id;
getAllRoomStates: SELECT * FROM room_states WHERE client_id = :client_id;
storeEvent: INSERT OR REPLACE INTO events (client_id, event_id, room_id, sort_order, origin_server_ts, sender, type, unsigned, content, prev_content, state_key, status) VALUES (:client_id, :event_id, :room_id, :sort_order, :origin_server_ts, :sender, :type, :unsigned, :content, :prev_content, :state_key, :status);
storeRoomState: INSERT OR REPLACE INTO room_states (client_id, event_id, room_id, sort_order, origin_server_ts, sender, type, unsigned, content, prev_content, state_key) VALUES (:client_id, :event_id, :room_id, :sort_order, :origin_server_ts, :sender, :type, :unsigned, :content, :prev_content, :state_key);
getAllRoomAccountData: SELECT * FROM room_account_data WHERE client_id = :client_id;
storeRoomAccountData: INSERT OR REPLACE INTO room_account_data (client_id, type, room_id, content) VALUES (:client_id, :type, :room_id, :content);
dbGetUser: SELECT * FROM room_states WHERE client_id = :client_id AND type = 'm.room.member' AND state_key = :state_key AND room_id = :room_id;
dbGetEventList: SELECT * FROM events WHERE client_id = :client_id AND room_id = :room_id GROUP BY event_id ORDER BY sort_order DESC;
getStates: SELECT * FROM room_states WHERE client_id = :client_id AND room_id = :room_id;
resetNotificationCount: UPDATE rooms SET notification_count = 0, highlight_count = 0 WHERE client_id = :client_id AND room_id = :room_id;
getEvent: SELECT * FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
removeEvent: DELETE FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
removeRoom: DELETE FROM rooms WHERE client_id = :client_id AND room_id = :room_id;
removeRoomEvents: DELETE FROM events WHERE client_id = :client_id AND room_id = :room_id;
storeFile: INSERT OR REPLACE INTO files (mxc_uri, bytes, saved_at) VALUES (:mxc_uri, :bytes, :time);
dbGetFile: SELECT * FROM files WHERE mxc_uri = :mxc_uri;

View file

@ -29,6 +29,7 @@ import 'package:http/http.dart' as http;
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
import './room.dart';
import 'utils/matrix_localizations.dart';
import './database/database.dart' show DbRoomState, DbEvent;
/// All data exchanged over Matrix is expressed as an "event". Typically each client action (e.g. sending a message) correlates with exactly one event.
class Event {
@ -96,6 +97,8 @@ class Event {
User get stateKeyUser => room.getUserByMXIDSync(stateKey);
double sortOrder;
Event(
{this.status = defaultStatus,
this.content,
@ -107,7 +110,8 @@ class Event {
this.unsigned,
this.prevContent,
this.stateKey,
this.room});
this.room,
this.sortOrder = 0.0});
static Map<String, dynamic> getMapFromPayload(dynamic payload) {
if (payload is String) {
@ -122,7 +126,7 @@ class Event {
}
/// Get a State event from a table row or from the event stream.
factory Event.fromJson(Map<String, dynamic> jsonPayload, Room room) {
factory Event.fromJson(Map<String, dynamic> jsonPayload, Room room, [double sortOrder]) {
final content = Event.getMapFromPayload(jsonPayload['content']);
final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']);
final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']);
@ -140,6 +144,31 @@ class Event {
: DateTime.now(),
unsigned: unsigned,
room: room,
sortOrder: sortOrder ?? 0.0,
);
}
/// Get an event from either DbRoomState or DbEvent
factory Event.fromDb(dynamic dbEntry, Room room) {
if (!(dbEntry is DbRoomState || dbEntry is DbEvent)) {
throw('Unknown db type');
}
final content = Event.getMapFromPayload(dbEntry.content);
final unsigned = Event.getMapFromPayload(dbEntry.unsigned);
final prevContent = Event.getMapFromPayload(dbEntry.prevContent);
return Event(
status: (dbEntry is DbEvent ? dbEntry.status : null) ?? defaultStatus,
stateKey: dbEntry.stateKey,
prevContent: prevContent,
content: content,
typeKey: dbEntry.type,
eventId: dbEntry.eventId,
roomId: dbEntry.roomId,
senderId: dbEntry.sender,
time: dbEntry.originServerTs ?? DateTime.now(),
unsigned: unsigned,
room: room,
sortOrder: dbEntry.sortOrder ?? 0.0,
);
}
@ -337,9 +366,7 @@ class Event {
/// from the database and the timelines. Returns false if not removed.
Future<bool> remove() async {
if (status < 1) {
if (room.client.store != null) {
await room.client.store.removeEvent(eventId);
}
await room.client.database?.removeEvent(room.client.id, eventId, room.id);
room.client.onEvent.add(EventUpdate(
roomID: room.id,
@ -349,7 +376,8 @@ class Event {
'event_id': eventId,
'status': -2,
'content': {'body': 'Removed...'}
}));
},
sortOrder: sortOrder));
return true;
}
return false;
@ -470,13 +498,13 @@ class Event {
// Is this file storeable?
final infoMap =
getThumbnail ? content['info']['thumbnail_info'] : content['info'];
final storeable = (room.client.storeAPI?.extended ?? false) &&
final storeable = room.client.database != null &&
infoMap is Map<String, dynamic> &&
infoMap['size'] is int &&
infoMap['size'] <= room.client.store.maxFileSize;
infoMap['size'] <= room.client.database.maxFileSize ;
if (storeable) {
uint8list = await room.client.store.getFile(mxContent.toString());
uint8list = await room.client.database.getFile(mxContent.toString());
}
// Download the file
@ -484,7 +512,7 @@ class Event {
uint8list =
(await http.get(mxContent.getDownloadLink(room.client))).bodyBytes;
if (storeable) {
await room.client.store.storeFile(uint8list, mxContent.toString());
await room.client.database.storeFile(mxContent.toString(), uint8list, DateTime.now());
}
}

View file

@ -21,6 +21,9 @@
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:famedlysdk/famedlysdk.dart';
import './database/database.dart' show DbPresence;
enum PresenceType { online, offline, unavailable }
/// Informs the client of a user's presence state change.
@ -39,6 +42,8 @@ class Presence {
final String statusMsg;
final DateTime time;
Presence({this.sender, this.displayname, this.avatarUrl, this.currentlyActive, this.lastActiveAgo, this.presence, this.statusMsg, this.time});
Presence.fromJson(Map<String, dynamic> json)
: sender = json['sender'],
displayname = json['content']['displayname'],
@ -55,4 +60,23 @@ class Presence {
e.toString() == "PresenceType.${json['content']['presence']}",
orElse: () => null),
statusMsg = json['content']['status_msg'];
factory Presence.fromDb(DbPresence dbEntry) {
final content = Event.getMapFromPayload(dbEntry.content);
return Presence(
sender: dbEntry.sender,
displayname: content['displayname'],
avatarUrl: content['avatar_url'] != null ? Uri.parse(content['avatar_url']) : null,
currentlyActive: content['currently_active'],
lastActiveAgo: content['last_active_ago'],
time: DateTime.fromMillisecondsSinceEpoch(
DateTime.now().millisecondsSinceEpoch -
(content['last_active_ago'] ?? 0)),
presence: PresenceType.values.firstWhere(
(e) =>
e.toString() == "PresenceType.${content['presence']}",
orElse: () => null),
statusMsg: content['status_msg'],
);
}
}

View file

@ -44,6 +44,7 @@ import 'timeline.dart';
import 'utils/matrix_localizations.dart';
import 'utils/states_map.dart';
import './utils/markdown.dart';
import './database/database.dart' show DbRoom;
enum PushRuleState { notify, mentions_only, dont_notify }
enum JoinRules { public, knock, invite, private }
@ -90,6 +91,27 @@ class Room {
List<String> _outboundGroupSessionDevices;
double _newestSortOrder;
double _oldestSortOrder;
double get newSortOrder {
_newestSortOrder++;
return _newestSortOrder;
}
double get oldSortOrder {
_oldestSortOrder--;
return _oldestSortOrder;
}
void resetSortOrder() {
_oldestSortOrder = _newestSortOrder = 0.0;
}
Future<void> updateSortOrder() async {
await client.database?.updateRoomSortOrder(_oldestSortOrder, _newestSortOrder, client.id, id);
}
/// Clears the existing outboundGroupSession, tries to create a new one and
/// stores it as an ingoingGroupSession in the [sessionKeys]. Then sends the
/// new session encrypted with olm to all non-blocked devices using
@ -120,7 +142,7 @@ class Room {
'session_id': outboundGroupSession.session_id(),
'session_key': outboundGroupSession.session_key(),
};
setSessionKey(rawSession['session_id'], rawSession);
setInboundGroupSession(rawSession['session_id'], rawSession);
try {
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
_outboundGroupSession = outboundGroupSession;
@ -137,12 +159,10 @@ class Room {
Future<void> _storeOutboundGroupSession() async {
if (_outboundGroupSession == null) return;
await client.storeAPI?.setItem(
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session',
_outboundGroupSession.pickle(client.userID));
await client.storeAPI?.setItem(
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session_devices',
json.encode(_outboundGroupSessionDevices));
await client.database?.storeOutboundGroupSession(
client.id, id, _outboundGroupSession.pickle(client.userID),
json.encode(_outboundGroupSessionDevices),
);
return;
}
@ -162,12 +182,11 @@ class Room {
return false;
}
}
_outboundGroupSessionDevices == null;
await client.storeAPI?.setItem(
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session', null);
await client.storeAPI?.setItem(
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session_devices',
null);
if (!wipe && _outboundGroupSessionDevices == null && _outboundGroupSession == null) {
return true; // let's just short-circuit out of here, no need to do DB stuff
}
_outboundGroupSessionDevices = null;
await client.database?.removeOutboundGroupSession(client.id, id);
_outboundGroupSession?.free();
_outboundGroupSession = null;
return true;
@ -181,13 +200,13 @@ class Room {
/// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
/// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
/// }
Map<String, SessionKey> get sessionKeys => _sessionKeys;
Map<String, SessionKey> _sessionKeys = {};
Map<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions;
Map<String, SessionKey> _inboundGroupSessions = {};
/// Add a new session key to the [sessionKeys].
void setSessionKey(String sessionId, Map<String, dynamic> content,
void setInboundGroupSession(String sessionId, Map<String, dynamic> content,
{bool forwarded = false}) {
if (sessionKeys.containsKey(sessionId)) return;
if (inboundGroupSessions.containsKey(sessionId)) return;
olm.InboundGroupSession inboundGroupSession;
if (content['algorithm'] == 'm.megolm.v1.aes-sha2') {
try {
@ -203,16 +222,17 @@ class Room {
e.toString());
}
}
_sessionKeys[sessionId] = SessionKey(
_inboundGroupSessions[sessionId] = SessionKey(
content: content,
inboundGroupSession: inboundGroupSession,
indexes: {},
key: client.userID,
);
if (_fullyRestored) {
client.storeAPI?.setItem(
'/clients/${client.deviceID}/rooms/${id}/session_keys',
json.encode(sessionKeys));
client.database?.storeInboundGroupSession(client.id, id, sessionId,
inboundGroupSession.pickle(client.userID), json.encode(content),
json.encode({}),
);
}
_tryAgainDecryptLastMessage();
onSessionKeyReceived.add(sessionId);
@ -395,7 +415,9 @@ class Room {
this.mInvitedMemberCount = 0,
this.mJoinedMemberCount = 0,
this.roomAccountData = const {},
});
double newestSortOrder = 0.0,
double oldestSortOrder = 0.0,
}) : _newestSortOrder = newestSortOrder, _oldestSortOrder = oldestSortOrder;
/// The default count of how much events should be requested when requesting the
/// history of this room.
@ -746,20 +768,22 @@ class Room {
};
}
final sortOrder = newSortOrder;
// Display a *sending* event and store it.
var eventUpdate =
EventUpdate(type: 'timeline', roomID: id, eventType: type, content: {
'type': type,
'event_id': messageID,
'sender': client.userID,
'status': 0,
'origin_server_ts': now,
'content': content
});
var eventUpdate = EventUpdate(type: 'timeline', roomID: id, eventType: type, sortOrder: sortOrder,
content: {
'type': type,
'event_id': messageID,
'sender': client.userID,
'status': 0,
'origin_server_ts': now,
'content': content
},
);
client.onEvent.add(eventUpdate);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
await client.database?.transaction(() async {
await client.database.storeEventUpdate(client.id, eventUpdate);
await updateSortOrder();
});
// Send the text and on success, store and display a *sent* event.
@ -767,7 +791,7 @@ class Room {
final response = await client.jsonRequest(
type: HTTPType.PUT,
action: '/client/r0/rooms/${id}/send/$sendType/$messageID',
data: client.encryptionEnabled
data: encrypted && client.encryptionEnabled
? await encryptGroupMessagePayload(content)
: content);
final String res = response['event_id'];
@ -775,9 +799,8 @@ class Room {
eventUpdate.content['unsigned'] = {'transaction_id': messageID};
eventUpdate.content['event_id'] = res;
client.onEvent.add(eventUpdate);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
await client.database?.transaction(() async {
await client.database.storeEventUpdate(client.id, eventUpdate);
});
return res;
} catch (exception) {
@ -786,9 +809,8 @@ class Room {
eventUpdate.content['status'] = -1;
eventUpdate.content['unsigned'] = {'transaction_id': messageID};
client.onEvent.add(eventUpdate);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
await client.database?.transaction(() async {
await client.database.storeEventUpdate(client.id, eventUpdate);
});
}
return null;
@ -809,7 +831,7 @@ class Room {
}
} on MatrixException catch (exception) {
if (exception.errorMessage == 'No known servers') {
await client.store?.forgetRoom(id);
await client.database?.forgetRoom(client.id, id);
client.onRoomUpdate.add(
RoomUpdate(
id: id,
@ -833,7 +855,7 @@ class Room {
/// Call the Matrix API to forget this room if you already left it.
Future<void> forget() async {
await client.store?.forgetRoom(id);
await client.database?.forgetRoom(client.id, id);
await client.jsonRequest(
type: HTTPType.POST, action: '/client/r0/rooms/${id}/forget');
return;
@ -903,65 +925,55 @@ class Room {
if (onHistoryReceived != null) onHistoryReceived();
prev_batch = resp['end'];
await client.store?.storeRoomPrevBatch(this);
final dbActions = <Future<dynamic> Function()>[];
if (client.database != null) {
dbActions.add(() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
}
if (!(resp['chunk'] is List<dynamic> &&
resp['chunk'].length > 0 &&
resp['end'] is String)) return;
if (resp['state'] is List<dynamic>) {
await client.store?.transaction(() {
for (var i = 0; i < resp['state'].length; i++) {
var eventUpdate = EventUpdate(
type: 'state',
roomID: id,
eventType: resp['state'][i]['type'],
content: resp['state'][i],
).decrypt(this);
client.onEvent.add(eventUpdate);
client.store.storeEventUpdate(eventUpdate);
}
return;
});
if (client.store == null) {
for (var i = 0; i < resp['state'].length; i++) {
var eventUpdate = EventUpdate(
type: 'state',
roomID: id,
eventType: resp['state'][i]['type'],
content: resp['state'][i],
).decrypt(this);
client.onEvent.add(eventUpdate);
for (final state in resp['state']) {
var eventUpdate = EventUpdate(
type: 'state',
roomID: id,
eventType: state['type'],
content: state,
sortOrder: oldSortOrder,
).decrypt(this);
client.onEvent.add(eventUpdate);
if (client.database != null) {
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate));
}
}
}
List<dynamic> history = resp['chunk'];
await client.store?.transaction(() {
for (var i = 0; i < history.length; i++) {
var eventUpdate = EventUpdate(
type: 'history',
roomID: id,
eventType: history[i]['type'],
content: history[i],
).decrypt(this);
client.onEvent.add(eventUpdate);
client.store.storeEventUpdate(eventUpdate);
client.store.setRoomPrevBatch(id, resp['end']);
}
return;
});
if (client.store == null) {
for (var i = 0; i < history.length; i++) {
var eventUpdate = EventUpdate(
type: 'history',
roomID: id,
eventType: history[i]['type'],
content: history[i],
).decrypt(this);
client.onEvent.add(eventUpdate);
for (final hist in history) {
var eventUpdate = EventUpdate(
type: 'history',
roomID: id,
eventType: hist['type'],
content: hist,
sortOrder: oldSortOrder,
).decrypt(this);
client.onEvent.add(eventUpdate);
if (client.database != null) {
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate));
}
}
if (client.database != null) {
dbActions.add(() => client.database.setRoomPrevBatch(resp['end'], client.id, id));
}
await client.database?.transaction(() async {
for (final f in dbActions) {
await f();
}
await updateSortOrder();
});
client.onRoomUpdate.add(
RoomUpdate(
id: id,
@ -1013,7 +1025,7 @@ class Room {
/// Sends *m.fully_read* and *m.read* for the given event ID.
Future<void> sendReadReceipt(String eventID) async {
notificationCount = 0;
await client?.store?.resetNotificationCount(id);
await client.database?.resetNotificationCount(client.id, id);
await client.jsonRequest(
type: HTTPType.POST,
action: '/client/r0/rooms/$id/read_markers',
@ -1024,48 +1036,44 @@ class Room {
return;
}
Future<void> restoreGroupSessionKeys() async {
Future<void> restoreGroupSessionKeys({
dynamic outboundGroupSession, // DbOutboundGroupSession, optionally as future
dynamic inboundGroupSessions, // DbSessionKey, as iterator and optionally as future
}) async {
// Restore the inbound and outbound session keys
if (client.encryptionEnabled && client.storeAPI != null) {
final String outboundGroupSessionPickle = await client.storeAPI.getItem(
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session');
if (outboundGroupSessionPickle != null) {
if (client.encryptionEnabled && client.database != null) {
outboundGroupSession ??= client.database.getDbOutboundGroupSession(client.id, id);
inboundGroupSessions ??= client.database.getDbInboundGroupSessions(client.id, id);
if (outboundGroupSession is Future) {
outboundGroupSession = await outboundGroupSession;
}
if (inboundGroupSessions is Future) {
inboundGroupSessions = await inboundGroupSessions;
}
if (outboundGroupSession != false && outboundGroupSession != null) {
try {
_outboundGroupSession = olm.OutboundGroupSession();
_outboundGroupSession.unpickle(
client.userID, outboundGroupSessionPickle);
client.userID, outboundGroupSession.pickle);
} catch (e) {
_outboundGroupSession = null;
print('[LibOlm] Unable to unpickle outboundGroupSession: ' +
e.toString());
}
}
final String outboundGroupSessionDevicesString = await client.storeAPI
.getItem(
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session_devices');
if (outboundGroupSessionDevicesString != null) {
_outboundGroupSessionDevices =
List<String>.from(json.decode(outboundGroupSessionDevicesString));
List<String>.from(json.decode(outboundGroupSession.deviceIds));
}
final String sessionKeysPickle = await client.storeAPI
.getItem('/clients/${client.deviceID}/rooms/${id}/session_keys');
if (sessionKeysPickle?.isNotEmpty ?? false) {
final Map<String, dynamic> map = json.decode(sessionKeysPickle);
_sessionKeys ??= {};
for (var entry in map.entries) {
if (inboundGroupSessions?.isNotEmpty ?? false) {
_inboundGroupSessions ??= {};
for (final sessionKey in inboundGroupSessions) {
try {
_sessionKeys[entry.key] =
SessionKey.fromJson(entry.value, client.userID);
_inboundGroupSessions[sessionKey.sessionId] = SessionKey.fromDb(sessionKey, client.userID);
} catch (e) {
print('[LibOlm] Could not unpickle inboundGroupSession: ' +
e.toString());
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
}
}
}
}
await client.storeAPI?.setItem(
'/clients/${client.deviceID}/rooms/${id}/session_keys',
json.encode(sessionKeys));
_tryAgainDecryptLastMessage();
_fullyRestored = true;
return;
@ -1076,44 +1084,64 @@ class Room {
/// Returns a Room from a json String which comes normally from the store. If the
/// state are also given, the method will await them.
static Future<Room> getRoomFromTableRow(
Map<String, dynamic> row, Client matrix,
{Future<List<Map<String, dynamic>>> states,
Future<List<Map<String, dynamic>>> roomAccountData}) async {
var newRoom = Room(
id: row['room_id'],
DbRoom row, // either Map<String, dynamic> or DbRoom
Client matrix,
{
dynamic states, // DbRoomState, as iterator and optionally as future
dynamic roomAccountData, // DbRoomAccountData, as iterator and optionally as future
dynamic outboundGroupSession, // DbOutboundGroupSession, optionally as future
dynamic inboundGroupSessions, // DbSessionKey, as iterator and optionally as future
}) async {
final newRoom = Room(
id: row.roomId,
membership: Membership.values
.firstWhere((e) => e.toString() == 'Membership.' + row['membership']),
notificationCount: row['notification_count'],
highlightCount: row['highlight_count'],
notificationSettings: row['notification_settings'],
prev_batch: row['prev_batch'],
mInvitedMemberCount: row['invited_member_count'],
mJoinedMemberCount: row['joined_member_count'],
mHeroes: row['heroes']?.split(',') ?? [],
.firstWhere((e) => e.toString() == 'Membership.' + row.membership),
notificationCount: row.notificationCount,
highlightCount: row.highlightCount,
notificationSettings: 'mention', // TODO: do proper things
prev_batch: row.prevBatch,
mInvitedMemberCount: row.invitedMemberCount,
mJoinedMemberCount: row.joinedMemberCount,
mHeroes: row.heroes?.split(',') ?? [],
client: matrix,
roomAccountData: {},
newestSortOrder: row.newestSortOrder,
oldestSortOrder: row.oldestSortOrder,
);
// Restore the inbound and outbound session keys
await newRoom.restoreGroupSessionKeys();
if (states != null) {
var rawStates = await states;
for (var i = 0; i < rawStates.length; i++) {
var newState = Event.fromJson(rawStates[i], newRoom);
var rawStates;
if (states is Future) {
rawStates = await states;
} else {
rawStates = states;
}
for (final rawState in rawStates) {
final newState = Event.fromDb(rawState, newRoom);;
newRoom.setState(newState);
}
}
var newRoomAccountData = <String, RoomAccountData>{};
if (roomAccountData != null) {
var rawRoomAccountData = await roomAccountData;
for (var i = 0; i < rawRoomAccountData.length; i++) {
var newData = RoomAccountData.fromJson(rawRoomAccountData[i], newRoom);
var rawRoomAccountData;
if (roomAccountData is Future) {
rawRoomAccountData = await roomAccountData;
} else {
rawRoomAccountData = roomAccountData;
}
for (final singleAccountData in rawRoomAccountData) {
final newData = RoomAccountData.fromDb(singleAccountData, newRoom);
newRoomAccountData[newData.typeKey] = newData;
}
newRoom.roomAccountData = newRoomAccountData;
}
newRoom.roomAccountData = newRoomAccountData;
// Restore the inbound and outbound session keys
await newRoom.restoreGroupSessionKeys(
outboundGroupSession: outboundGroupSession,
inboundGroupSessions: inboundGroupSessions,
);
return newRoom;
}
@ -1122,24 +1150,28 @@ class Room {
Future<Timeline> getTimeline(
{onTimelineUpdateCallback onUpdate,
onTimelineInsertCallback onInsert}) async {
var events = client.store != null
? await client.store.getEventList(this)
: <Event>[];
var events;
if (client.database != null) {
events = await client.database.getEventList(client.id, this);
} else {
events = <Event>[];
}
// Try again to decrypt encrypted events and update the database.
if (encrypted && client.store != null) {
await client.store.transaction(() {
if (encrypted && client.database != null) {
await client.database.transaction(() async {
for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted &&
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
events[i] = events[i].decrypted;
if (events[i].type != EventTypes.Encrypted) {
client.store.storeEventUpdate(
await client.database.storeEventUpdate(client.id,
EventUpdate(
eventType: events[i].typeKey,
content: events[i].toJson(),
roomID: events[i].roomId,
type: 'timeline',
sortOrder: events[i].sortOrder,
),
);
}
@ -1154,7 +1186,7 @@ class Room {
onUpdate: onUpdate,
onInsert: onInsert,
);
if (client.store == null) {
if (client.database == null) {
prev_batch = '';
await requestHistory(historyCount: 10);
}
@ -1227,6 +1259,9 @@ class Room {
/// Requests a missing [User] for this room. Important for clients using
/// lazy loading.
Future<User> requestUser(String mxID, {bool ignoreErrors = false}) async {
if (getState('m.room.member', mxID) != null) {
return getState('m.room.member', mxID).asUser;
}
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
Map<String, dynamic> resp;
try {
@ -1237,23 +1272,30 @@ class Room {
_requestingMatrixIds.remove(mxID);
if (!ignoreErrors) rethrow;
}
if (resp == null) {
return null;
}
final user = User(mxID,
displayName: resp['displayname'],
avatarUrl: resp['avatar_url'],
room: this);
states[mxID] = user;
if (client.store != null) {
await client.store.transaction(() {
client.store.storeEventUpdate(
EventUpdate(
content: resp,
roomID: id,
type: 'state',
eventType: 'm.room.member'),
);
return;
});
}
await client.database?.transaction(() async {
final content = <String, dynamic>{
'sender': mxID,
'type': 'm.room.member',
'content': resp,
'state_key': mxID,
};
await client.database.storeEventUpdate(client.id,
EventUpdate(
content: content,
roomID: id,
type: 'state',
eventType: 'm.room.member',
sortOrder: 0.0),
);
});
if (onUpdate != null) onUpdate.add(id);
_requestingMatrixIds.remove(mxID);
return user;
@ -1690,12 +1732,11 @@ class Room {
Future<List<DeviceKeys>> getUserDeviceKeys() async {
var deviceKeys = <DeviceKeys>[];
var users = await requestParticipants();
for (final userDeviceKeyEntry in client.userDeviceKeys.entries) {
if (users.indexWhere((u) => u.id == userDeviceKeyEntry.key) == -1) {
continue;
}
for (var deviceKeyEntry in userDeviceKeyEntry.value.deviceKeys.values) {
deviceKeys.add(deviceKeyEntry);
for (final user in users) {
if (client.userDeviceKeys.containsKey(user.id)) {
for (var deviceKeyEntry in client.userDeviceKeys[user.id].deviceKeys.values) {
deviceKeys.add(deviceKeyEntry);
}
}
}
return deviceKeys;
@ -1745,23 +1786,23 @@ class Room {
throw (DecryptError.UNKNOWN_ALGORITHM);
}
final String sessionId = event.content['session_id'];
if (!sessionKeys.containsKey(sessionId)) {
if (!inboundGroupSessions.containsKey(sessionId)) {
throw (DecryptError.UNKNOWN_SESSION);
}
final decryptResult = sessionKeys[sessionId]
final decryptResult = inboundGroupSessions[sessionId]
.inboundGroupSession
.decrypt(event.content['ciphertext']);
final messageIndexKey =
event.eventId + event.time.millisecondsSinceEpoch.toString();
if (sessionKeys[sessionId].indexes.containsKey(messageIndexKey) &&
sessionKeys[sessionId].indexes[messageIndexKey] !=
if (inboundGroupSessions[sessionId].indexes.containsKey(messageIndexKey) &&
inboundGroupSessions[sessionId].indexes[messageIndexKey] !=
decryptResult.message_index) {
if ((_outboundGroupSession?.session_id() ?? '') == sessionId) {
clearOutboundGroupSession();
}
throw (DecryptError.CHANNEL_CORRUPTED);
}
sessionKeys[sessionId].indexes[messageIndexKey] =
inboundGroupSessions[sessionId].indexes[messageIndexKey] =
decryptResult.message_index;
_storeOutboundGroupSession();
decryptedPayload = json.decode(decryptResult.plaintext);
@ -1799,6 +1840,7 @@ class Room {
stateKey: event.stateKey,
prevContent: event.prevContent,
status: event.status,
sortOrder: event.sortOrder,
);
}
}

View file

@ -24,6 +24,7 @@
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/account_data.dart';
import 'package:famedlysdk/src/event.dart';
import './database/database.dart' show DbRoomAccountData;
/// Stripped down events for account data and ephemrals of a room.
class RoomAccountData extends AccountData {
@ -46,4 +47,14 @@ class RoomAccountData extends AccountData {
roomId: jsonPayload['room_id'],
room: room);
}
/// get room account data from DbRoomAccountData
factory RoomAccountData.fromDb(DbRoomAccountData dbEntry, Room room) {
final content = Event.getMapFromPayload(dbEntry.content);
return RoomAccountData(
content: content,
typeKey: dbEntry.type,
roomId: dbEntry.roomId,
room: room);
}
}

View file

@ -1,129 +0,0 @@
/*
* Copyright (c) 2019 Zender & Kurtz GbR.
*
* Authors:
* Christian Pauly <krille@famedly.com>
* Marcel Radzio <mtrnord@famedly.com>
*
* This file is part of famedlysdk.
*
* famedlysdk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* famedlysdk 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'dart:core';
import 'dart:typed_data';
import 'package:famedlysdk/src/account_data.dart';
import 'package:famedlysdk/src/presence.dart';
import 'package:famedlysdk/src/utils/device_keys_list.dart';
import 'client.dart';
import 'event.dart';
import 'room.dart';
import 'user.dart';
import 'sync/event_update.dart';
import 'sync/room_update.dart';
import 'sync/user_update.dart';
abstract class StoreAPI {
/// Whether this is a simple store which only stores the client credentials and
/// end to end encryption stuff or the whole sync payloads.
final bool extended = false;
/// Link back to the client.
Client client;
/// Will be automatically called when the client is logged in successfully.
Future<void> storeClient();
/// Clears all tables from the database.
Future<void> clear();
Future<dynamic> getItem(String key);
Future<void> setItem(String key, String value);
Future<Map<String, DeviceKeysList>> getUserDeviceKeys();
Future<void> storeUserDeviceKeys(Map<String, DeviceKeysList> userDeviceKeys);
}
/// Responsible to store all data persistent and to query objects from the
/// database.
abstract class ExtendedStoreAPI extends StoreAPI {
/// The maximum size of files which should be stored in bytes.
int get maxFileSize => 1 * 1024 * 1024;
/// Whether this is a simple store which only stores the client credentials and
/// end to end encryption stuff or the whole sync payloads.
@override
final bool extended = true;
/// The current trans
Future<void> setRoomPrevBatch(String roomId, String prevBatch);
/// Performs these query or queries inside of an transaction.
Future<void> transaction(void Function() queries);
/// Will be automatically called on every synchronisation. Must be called inside of
// /// [transaction].
void storePrevBatch(String prevBatch);
Future<void> storeRoomPrevBatch(Room room);
/// Stores a RoomUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeRoomUpdate(RoomUpdate roomUpdate);
/// Stores an UserUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeUserEventUpdate(UserUpdate userUpdate);
/// Stores an EventUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeEventUpdate(EventUpdate eventUpdate);
/// Returns a User object by a given Matrix ID and a Room.
Future<User> getUser({String matrixID, Room room});
/// Returns a list of events for the given room and sets all participants.
Future<List<Event>> getEventList(Room room);
/// Returns all rooms, the client is participating. Excludes left rooms.
Future<List<Room>> getRoomList({bool onlyLeft = false});
/// Deletes this room from the database.
Future<void> forgetRoom(String roomID);
/// Sets notification and highlight count to 0 for this room.
Future<void> resetNotificationCount(String roomID);
/// Searches for the event in the store.
Future<Event> getEventById(String eventID, Room room);
/// Returns all account data for this client.
Future<Map<String, AccountData>> getAccountData();
/// Returns all stored presences for this client.
Future<Map<String, Presence>> getPresences();
/// Removes this event from the store.
Future removeEvent(String eventId);
/// Stores the bytes of this file indexed by the [mxcUri]. Throws an
/// exception if the bytes are more than [MAX_FILE_SIZE].
Future<void> storeFile(Uint8List bytes, String mxcUri);
/// Returns the file bytes indexed by [mxcUri]. Returns null if not found.
Future<Uint8List> getFile(String mxcUri);
}

View file

@ -40,7 +40,10 @@ class EventUpdate {
// The json payload of the content of this event.
final Map<String, dynamic> content;
EventUpdate({this.eventType, this.roomID, this.type, this.content});
// the order where to stort this event
final double sortOrder;
EventUpdate({this.eventType, this.roomID, this.type, this.content, this.sortOrder});
EventUpdate decrypt(Room room) {
if (eventType != 'm.room.encrypted') {
@ -48,12 +51,13 @@ class EventUpdate {
}
try {
var decrpytedEvent =
room.decryptGroupMessage(Event.fromJson(content, room));
room.decryptGroupMessage(Event.fromJson(content, room, sortOrder));
return EventUpdate(
eventType: eventType,
roomID: roomID,
type: type,
content: decrpytedEvent.toJson(),
sortOrder: sortOrder,
);
} catch (e) {
print('[LibOlm] Could not decrypt megolm event: ' + e.toString());

View file

@ -122,7 +122,7 @@ class Timeline {
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
if (eventId != null) {
events[eventId]
.setRedactionEvent(Event.fromJson(eventUpdate.content, room));
.setRedactionEvent(Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder));
}
} else if (eventUpdate.content['status'] == -2) {
var i = _findEvent(event_id: eventUpdate.content['event_id']);
@ -138,18 +138,18 @@ class Timeline {
: null);
if (i < events.length) {
events[i] = Event.fromJson(eventUpdate.content, room);
events[i] = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
}
} else {
Event newEvent;
var senderUser = await room.client.store
?.getUser(matrixID: eventUpdate.content['sender'], room: room);
var senderUser = room.getState('m.room.member', eventUpdate.content['sender'])?.asUser ?? await room.client.database
?.getUser(room.client.id, eventUpdate.content['sender'], room);
if (senderUser != null) {
eventUpdate.content['displayname'] = senderUser.displayName;
eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString();
}
newEvent = Event.fromJson(eventUpdate.content, room);
newEvent = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
if (eventUpdate.type == 'history' &&
events.indexWhere(
@ -173,8 +173,7 @@ class Timeline {
void sort() {
if (sortLock || events.length < 2) return;
sortLock = true;
events?.sort((a, b) =>
b.time.millisecondsSinceEpoch.compareTo(a.time.millisecondsSinceEpoch));
events?.sort((a, b) => b.sortOrder - a.sortOrder > 0 ? 1 : -1);
sortLock = false;
}

View file

@ -1,12 +1,23 @@
import 'dart:convert';
import '../client.dart';
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
import '../event.dart';
class DeviceKeysList {
String userId;
bool outdated = true;
Map<String, DeviceKeys> deviceKeys = {};
DeviceKeysList.fromDb(DbUserDeviceKey dbEntry, List<DbUserDeviceKeysKey> childEntries) {
userId = dbEntry.userId;
outdated = dbEntry.outdated;
deviceKeys = {};
for (final childEntry in childEntries) {
deviceKeys[childEntry.deviceId] = DeviceKeys.fromDb(childEntry);
}
}
DeviceKeysList.fromJson(Map<String, dynamic> json) {
userId = json['user_id'];
outdated = json['outdated'];
@ -52,7 +63,7 @@ class DeviceKeys {
Future<void> setVerified(bool newVerified, Client client) {
verified = newVerified;
return client.storeAPI.storeUserDeviceKeys(client.userDeviceKeys);
return client.database?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
}
Future<void> setBlocked(bool newBlocked, Client client) {
@ -63,7 +74,7 @@ class DeviceKeys {
room.clearOutboundGroupSession();
}
}
return client.storeAPI.storeUserDeviceKeys(client.userDeviceKeys);
return client.database?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
}
DeviceKeys({
@ -77,6 +88,22 @@ class DeviceKeys {
this.blocked,
});
DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry) {
final content = Event.getMapFromPayload(dbEntry.content);
userId = dbEntry.userId;
deviceId = dbEntry.deviceId;
algorithms = content['algorithms'].cast<String>();
keys = content['keys'] != null ? Map<String, String>.from(content['keys']) : null;
signatures = content['signatures'] != null
? Map<String, dynamic>.from(content['signatures'])
: null;
unsigned = content['unsigned'] != null
? Map<String, dynamic>.from(content['unsigned'])
: null;
verified = dbEntry.verified;
blocked = dbEntry.blocked;
}
DeviceKeys.fromJson(Map<String, dynamic> json) {
userId = json['user_id'];
deviceId = json['device_id'];

View file

@ -16,7 +16,7 @@ class RoomKeyRequest extends ToDeviceEvent {
Future<void> forwardKey() async {
var room = this.room;
final session = room.sessionKeys[content['body']['session_id']];
final session = room.inboundGroupSessions[content['body']['session_id']];
var forwardedKeys = <dynamic>[client.identityKey];
for (final key in session.forwardingCurve25519KeyChain) {
forwardedKeys.add(key);

View file

@ -2,6 +2,9 @@ import 'dart:convert';
import 'package:olm/olm.dart';
import '../database/database.dart' show DbInboundGroupSession;
import '../event.dart';
class SessionKey {
Map<String, dynamic> content;
Map<String, int> indexes;
@ -14,6 +17,20 @@ class SessionKey {
SessionKey({this.content, this.inboundGroupSession, this.key, this.indexes});
SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key {
final parsedContent = Event.getMapFromPayload(dbEntry.content);
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes);
content = parsedContent != null
? Map<String, dynamic>.from(parsedContent)
: null;
indexes = parsedIndexes != null
? Map<String, int>.from(parsedIndexes)
: <String, int>{};
var newInboundGroupSession = InboundGroupSession();
newInboundGroupSession.unpickle(key, dbEntry.pickle);
inboundGroupSession = newInboundGroupSession;
}
SessionKey.fromJson(Map<String, dynamic> json, String key) : key = key {
content = json['content'] != null
? Map<String, dynamic>.from(json['content'])

View file

@ -1,13 +1,27 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.36.3"
version: "0.39.8"
analyzer_plugin_fork:
dependency: transitive
description:
name: analyzer_plugin_fork
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2"
archive:
dependency: transitive
description:
@ -42,42 +56,42 @@ packages:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.4"
version: "1.2.2"
build_config:
dependency: transitive
description:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
version: "0.4.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "2.1.4"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
version: "1.3.7"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.2"
version: "1.9.0"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6"
version: "5.1.0"
built_collection:
dependency: transitive
description:
@ -91,7 +105,7 @@ packages:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "6.6.0"
version: "7.1.0"
canonical_json:
dependency: "direct main"
description:
@ -106,13 +120,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
version: "3.2.1"
collection:
dependency: transitive
description:
@ -154,7 +182,7 @@ packages:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.7"
version: "1.3.6"
ffi:
dependency: transitive
description:
@ -169,13 +197,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.9"
front_end:
dependency: transitive
description:
name: front_end
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.18"
glob:
dependency: transitive
description:
@ -252,14 +273,7 @@ packages:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
kernel:
dependency: transitive
description:
name: kernel
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.18"
version: "3.0.1"
logging:
dependency: transitive
description:
@ -311,6 +325,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
moor:
dependency: "direct main"
description:
name: moor
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
moor_ffi:
dependency: "direct dev"
description:
name: moor_ffi
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
moor_generator:
dependency: "direct dev"
description:
name: moor_generator
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
multi_server_socket:
dependency: transitive
description:
@ -396,7 +431,7 @@ packages:
name: pubspec_parse
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
version: "0.1.5"
quiver:
dependency: transitive
description:
@ -404,6 +439,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
recase:
dependency: transitive
description:
name: recase
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
shelf:
dependency: transitive
description:
@ -432,6 +474,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.3"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.5"
source_map_stack_trace:
dependency: transitive
description:
@ -453,6 +502,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.5"
sqlparser:
dependency: transitive
description:
name: sqlparser
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.1"
stack_trace:
dependency: transitive
description:
@ -473,7 +529,7 @@ packages:
name: stream_transform
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.19"
version: "1.2.0"
string_scanner:
dependency: transitive
description:
@ -481,6 +537,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
synchronized:
dependency: transitive
description:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
term_glyph:
dependency: transitive
description:
@ -515,7 +578,7 @@ packages:
name: timing
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1+1"
version: "0.1.1+2"
typed_data:
dependency: transitive
description:

View file

@ -14,6 +14,7 @@ dependencies:
image: ^2.1.4
markdown: ^2.1.3
html_unescape: ^1.0.1+3
moor: ^3.0.2
olm:
git:
@ -27,5 +28,7 @@ dependencies:
dev_dependencies:
test: ^1.0.0
moor_generator: ^3.0.0
build_runner: ^1.5.2
pedantic: ^1.9.0 # DO NOT UPDATE AS THIS WOULD CAUSE FLUTTER TO FAIL
moor_ffi: ^0.5.0

View file

@ -39,7 +39,7 @@ import 'package:olm/olm.dart' as olm;
import 'package:test/test.dart';
import 'fake_matrix_api.dart';
import 'fake_store.dart';
import 'fake_database.dart';
void main() {
Client matrix;
@ -86,7 +86,6 @@ void main() {
});
expect(matrix.homeserver, null);
expect(matrix.matrixVersions, null);
try {
await matrix.checkServer('https://fakeserver.wrongaddress');
@ -95,8 +94,6 @@ void main() {
}
await matrix.checkServer('https://fakeserver.notexisting');
expect(matrix.homeserver, 'https://fakeserver.notexisting');
expect(matrix.matrixVersions,
['r0.0.1', 'r0.1.0', 'r0.2.0', 'r0.3.0', 'r0.4.0', 'r0.5.0']);
final resp = await matrix
.jsonRequest(type: HTTPType.POST, action: '/client/r0/login', data: {
@ -128,7 +125,6 @@ void main() {
newHomeserver: matrix.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: resp['device_id'],
newMatrixVersions: matrix.matrixVersions,
newOlmAccount: pickledOlmAccount,
);
@ -160,18 +156,18 @@ void main() {
expect(matrix.directChats, matrix.accountData['m.direct'].content);
expect(matrix.presences.length, 1);
expect(matrix.rooms[1].ephemerals.length, 2);
expect(matrix.rooms[1].sessionKeys.length, 1);
expect(matrix.rooms[1].inboundGroupSessions.length, 1);
expect(
matrix
.rooms[1]
.sessionKeys['ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
.inboundGroupSessions['ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
.content['session_key'],
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw');
if (olmEnabled) {
expect(
matrix
.rooms[1]
.sessionKeys['ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
.inboundGroupSessions['ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
.inboundGroupSession !=
null,
true);
@ -279,7 +275,6 @@ void main() {
expect(matrix.userID == null, true);
expect(matrix.deviceID == null, true);
expect(matrix.deviceName == null, true);
expect(matrix.matrixVersions == null, true);
expect(matrix.prevBatch == null, true);
var loginState = await loginStateFuture;
@ -635,8 +630,7 @@ void main() {
test('Test the fake store api', () async {
var client1 = Client('testclient', debug: true);
client1.httpClient = FakeMatrixApi();
var fakeStore = FakeStore(client1, {});
client1.storeAPI = fakeStore;
client1.database = getDatabase();
client1.connect(
newToken: 'abc123',
@ -644,14 +638,6 @@ void main() {
newHomeserver: 'https://fakeServer.notExisting',
newDeviceName: 'Text Matrix Client',
newDeviceID: 'GHTYAJCE',
newMatrixVersions: [
'r0.0.1',
'r0.1.0',
'r0.2.0',
'r0.3.0',
'r0.4.0',
'r0.5.0'
],
newOlmAccount: pickledOlmAccount,
);
@ -669,8 +655,9 @@ void main() {
var client2 = Client('testclient', debug: true);
client2.httpClient = FakeMatrixApi();
client2.storeAPI = FakeStore(client2, fakeStore.storeMap);
client2.database = client1.database;
client2.connect();
await Future.delayed(Duration(milliseconds: 100));
expect(client2.isLogged(), true);
@ -679,11 +666,10 @@ void main() {
expect(client2.homeserver, client1.homeserver);
expect(client2.deviceID, client1.deviceID);
expect(client2.deviceName, client1.deviceName);
expect(client2.matrixVersions, client1.matrixVersions);
if (client2.encryptionEnabled) {
expect(client2.pickledOlmAccount, client1.pickledOlmAccount);
expect(json.encode(client2.rooms[1].sessionKeys[sessionKey]),
json.encode(client1.rooms[1].sessionKeys[sessionKey]));
expect(json.encode(client2.rooms[1].inboundGroupSessions[sessionKey]),
json.encode(client1.rooms[1].inboundGroupSessions[sessionKey]));
expect(client2.rooms[1].id, client1.rooms[1].id);
expect(client2.rooms[1].outboundGroupSession.session_key(), sessionKey);
}

6
test/fake_database.dart Normal file
View file

@ -0,0 +1,6 @@
import 'package:famedlysdk/famedlysdk.dart';
import 'package:moor_ffi/moor_ffi.dart' as moor;
Database getDatabase() {
return Database(moor.VmDatabase.memory());
}

View file

@ -1,95 +0,0 @@
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart';
class FakeStore implements StoreAPI {
/// Whether this is a simple store which only stores the client credentials and
/// end to end encryption stuff or the whole sync payloads.
@override
final bool extended = false;
Map<String, dynamic> storeMap = {};
/// Link back to the client.
@override
Client client;
FakeStore(this.client, this.storeMap) {
_init();
}
Future<void> _init() async {
final credentialsStr = await getItem(client.clientName);
if (credentialsStr == null || credentialsStr.isEmpty) {
client.onLoginStateChanged.add(LoginState.loggedOut);
return;
}
print('[Matrix] Restoring account credentials');
final Map<String, dynamic> credentials = json.decode(credentialsStr);
client.connect(
newDeviceID: credentials['deviceID'],
newDeviceName: credentials['deviceName'],
newHomeserver: credentials['homeserver'],
newMatrixVersions: List<String>.from(credentials['matrixVersions']),
newToken: credentials['token'],
newUserID: credentials['userID'],
newPrevBatch: credentials['prev_batch'],
newOlmAccount: credentials['olmAccount'],
);
}
/// Will be automatically called when the client is logged in successfully.
@override
Future<void> storeClient() async {
final credentials = {
'deviceID': client.deviceID,
'deviceName': client.deviceName,
'homeserver': client.homeserver,
'matrixVersions': client.matrixVersions,
'token': client.accessToken,
'userID': client.userID,
'olmAccount': client.pickledOlmAccount,
};
await setItem(client.clientName, json.encode(credentials));
return;
}
/// Clears all tables from the database.
@override
Future<void> clear() async {
storeMap = {};
return;
}
@override
Future<dynamic> getItem(String key) async {
return storeMap[key];
}
@override
Future<void> setItem(String key, String value) async {
storeMap[key] = value;
return;
}
String get _UserDeviceKeysKey => '${client.clientName}.user_device_keys';
@override
Future<Map<String, DeviceKeysList>> getUserDeviceKeys() async {
final deviceKeysListString = await getItem(_UserDeviceKeysKey);
if (deviceKeysListString == null) return {};
Map<String, dynamic> rawUserDeviceKeys = json.decode(deviceKeysListString);
var userDeviceKeys = <String, DeviceKeysList>{};
for (final entry in rawUserDeviceKeys.entries) {
userDeviceKeys[entry.key] = DeviceKeysList.fromJson(entry.value);
}
return userDeviceKeys;
}
@override
Future<void> storeUserDeviceKeys(
Map<String, DeviceKeysList> userDeviceKeys) async {
await setItem(_UserDeviceKeysKey, json.encode(userDeviceKeys));
}
}

View file

@ -25,7 +25,7 @@ import 'package:famedlysdk/famedlysdk.dart';
import 'package:test/test.dart';
import 'fake_matrix_api.dart';
import 'fake_store.dart';
import 'fake_database.dart';
void main() {
/// All Tests related to device keys
@ -53,13 +53,13 @@ void main() {
var matrix = Client('testclient', debug: true);
matrix.httpClient = FakeMatrixApi();
matrix.storeAPI = FakeStore(matrix, {});
matrix.database = getDatabase();
await matrix.checkServer('https://fakeServer.notExisting');
await matrix.login('test', '1234');
var room = matrix.getRoomById('!726s6s6q:example.com');
if (matrix.encryptionEnabled) {
await room.createOutboundGroupSession();
rawJson['content']['body']['session_id'] = room.sessionKeys.keys.first;
rawJson['content']['body']['session_id'] = room.inboundGroupSessions.keys.first;
var roomKeyRequest = RoomKeyRequest.fromToDeviceEvent(
ToDeviceEvent.fromJson(rawJson), matrix);

View file

@ -26,6 +26,7 @@ import 'package:famedlysdk/src/event.dart';
import 'package:famedlysdk/src/room.dart';
import 'package:famedlysdk/src/user.dart';
import 'package:famedlysdk/src/utils/matrix_file.dart';
import 'package:famedlysdk/src/database/database.dart' show DbRoom, DbRoomState, DbRoomAccountData;
import 'package:test/test.dart';
import 'fake_matrix_api.dart';
@ -62,44 +63,50 @@ void main() {
'@charley:example.org'
];
var jsonObj = <String, dynamic>{
'room_id': id,
'membership': membership.toString().split('.').last,
'avatar_url': '',
'notification_count': notificationCount,
'highlight_count': highlightCount,
'prev_batch': '',
'joined_member_count': notificationCount,
'invited_member_count': notificationCount,
'heroes': heroes.join(','),
};
var dbRoom = DbRoom(
clientId: 1,
roomId: id,
membership: membership.toString().split('.').last,
highlightCount: highlightCount,
notificationCount: notificationCount,
prevBatch: '',
joinedMemberCount: notificationCount,
invitedMemberCount: notificationCount,
newestSortOrder: 0.0,
oldestSortOrder: 0.0,
heroes: heroes.join(','),
);
Function states = () async => [
{
'content': {'join_rule': 'public'},
'event_id': '143273582443PhrSn:example.org',
'origin_server_ts': 1432735824653,
'room_id': id,
'sender': '@example:example.org',
'state_key': '',
'type': 'm.room.join_rules',
'unsigned': {'age': 1234}
}
];
var states = [
DbRoomState(
clientId: 1,
eventId: '143273582443PhrSn:example.org',
roomId: id,
sortOrder: 0.0,
originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653),
sender: '@example:example.org',
type: 'm.room.join_rules',
unsigned: '{"age": 1234}',
content: '{"join_rule": "public"}',
prevContent: '',
stateKey: '',
),
];
Function roomAccountData = () async => [
{
'content': {'foo': 'bar'},
'room_id': id,
'type': 'com.test.foo'
}
];
var roomAccountData = [
DbRoomAccountData(
clientId: 1,
type: 'com.test.foo',
roomId: id,
content: '{"foo": "bar"}',
),
];
room = await Room.getRoomFromTableRow(
jsonObj,
dbRoom,
matrix,
states: states(),
roomAccountData: roomAccountData(),
states: states,
roomAccountData: roomAccountData,
);
expect(room.id, id);
@ -390,14 +397,14 @@ void main() {
expect(room.outboundGroupSession != null, true);
expect(room.outboundGroupSession.session_id().isNotEmpty, true);
expect(
room.sessionKeys.containsKey(room.outboundGroupSession.session_id()),
room.inboundGroupSessions.containsKey(room.outboundGroupSession.session_id()),
true);
expect(
room.sessionKeys[room.outboundGroupSession.session_id()]
room.inboundGroupSessions[room.outboundGroupSession.session_id()]
.content['session_key'],
room.outboundGroupSession.session_key());
expect(
room.sessionKeys[room.outboundGroupSession.session_id()].indexes
room.inboundGroupSessions[room.outboundGroupSession.session_id()].indexes
.length,
0);
});

View file

@ -65,7 +65,8 @@ void main() {
'status': 2,
'event_id': '1',
'origin_server_ts': testTimeStamp
}));
},
sortOrder: room.newSortOrder));
client.onEvent.add(EventUpdate(
type: 'timeline',
@ -78,7 +79,8 @@ void main() {
'status': 2,
'event_id': '2',
'origin_server_ts': testTimeStamp - 1000
}));
},
sortOrder: room.oldSortOrder));
expect(timeline.sub != null, true);
@ -125,7 +127,8 @@ void main() {
'redacts': '2',
'event_id': '3',
'origin_server_ts': testTimeStamp + 1000
}));
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
@ -159,7 +162,8 @@ void main() {
'event_id': '42',
'unsigned': {'transaction_id': '1234'},
'origin_server_ts': DateTime.now().millisecondsSinceEpoch
}));
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
@ -182,7 +186,8 @@ void main() {
'status': 0,
'event_id': 'abc',
'origin_server_ts': testTimeStamp
}));
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
await room.sendTextEvent('test', txid: 'errortxid');
await Future.delayed(Duration(milliseconds: 50));
@ -230,9 +235,9 @@ void main() {
expect(updateCount, 20);
expect(timeline.events.length, 9);
expect(timeline.events[6].eventId, '1143273582443PhrSn:example.org');
expect(timeline.events[6].eventId, '3143273582443PhrSn:example.org');
expect(timeline.events[7].eventId, '2143273582443PhrSn:example.org');
expect(timeline.events[8].eventId, '3143273582443PhrSn:example.org');
expect(timeline.events[8].eventId, '1143273582443PhrSn:example.org');
expect(room.prev_batch, 't47409-4357353_219380_26003_2265');
});
});

View file

@ -1,5 +1,5 @@
import 'package:famedlysdk/famedlysdk.dart';
import '../test/fake_store.dart';
import '../test/fake_database.dart';
void main() => test();
@ -18,14 +18,14 @@ const String testMessage6 = 'Hello mars';
void test() async {
print('++++ Login $testUserA ++++');
var testClientA = Client('TestClient', debug: false);
testClientA.storeAPI = FakeStore(testClientA, <String, dynamic>{});
testClientA.database = getDatabase();
await testClientA.checkServer(homeserver);
await testClientA.login(testUserA, testPasswordA);
assert(testClientA.encryptionEnabled);
print('++++ Login $testUserB ++++');
var testClientB = Client('TestClient', debug: false);
testClientB.storeAPI = FakeStore(testClientB, <String, dynamic>{});
testClientB.database = getDatabase();
await testClientB.checkServer(homeserver);
await testClientB.login(testUserB, testPasswordA);
assert(testClientB.encryptionEnabled);
@ -128,12 +128,12 @@ void test() async {
await Future.delayed(Duration(seconds: 5));
assert(room.outboundGroupSession != null);
var currentSessionIdA = room.outboundGroupSession.session_id();
assert(room.sessionKeys.containsKey(room.outboundGroupSession.session_id()));
assert(room.inboundGroupSessions.containsKey(room.outboundGroupSession.session_id()));
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
testClientB.olmSessions[testClientA.identityKey].first.session_id());
assert(inviteRoom.sessionKeys
assert(inviteRoom.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(room.lastMessage == testMessage);
assert(inviteRoom.lastMessage == testMessage);
@ -149,7 +149,7 @@ void test() async {
testClientB.olmSessions[testClientA.identityKey].first.session_id());
assert(room.outboundGroupSession.session_id() == currentSessionIdA);
assert(inviteRoom.sessionKeys
assert(inviteRoom.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(room.lastMessage == testMessage2);
assert(inviteRoom.lastMessage == testMessage2);
@ -163,9 +163,9 @@ void test() async {
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(room.outboundGroupSession.session_id() == currentSessionIdA);
assert(inviteRoom.outboundGroupSession != null);
assert(inviteRoom.sessionKeys
assert(inviteRoom.inboundGroupSessions
.containsKey(inviteRoom.outboundGroupSession.session_id()));
assert(room.sessionKeys
assert(room.inboundGroupSessions
.containsKey(inviteRoom.outboundGroupSession.session_id()));
assert(inviteRoom.lastMessage == testMessage3);
assert(room.lastMessage == testMessage3);
@ -174,7 +174,7 @@ void test() async {
print('++++ Login $testUserB in another client ++++');
var testClientC = Client('TestClient', debug: false);
testClientC.storeAPI = FakeStore(testClientC, <String, dynamic>{});
testClientC.database = getDatabase();
await testClientC.checkServer(homeserver);
await testClientC.login(testUserB, testPasswordA);
await Future.delayed(Duration(seconds: 3));
@ -193,7 +193,7 @@ void test() async {
testClientC.olmSessions[testClientA.identityKey].first.session_id());
assert(room.outboundGroupSession.session_id() != currentSessionIdA);
currentSessionIdA = room.outboundGroupSession.session_id();
assert(inviteRoom.sessionKeys
assert(inviteRoom.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(room.lastMessage == testMessage4);
assert(inviteRoom.lastMessage == testMessage4);
@ -216,7 +216,7 @@ void test() async {
testClientB.olmSessions[testClientA.identityKey].first.session_id());
assert(room.outboundGroupSession.session_id() != currentSessionIdA);
currentSessionIdA = room.outboundGroupSession.session_id();
assert(inviteRoom.sessionKeys
assert(inviteRoom.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(room.lastMessage == testMessage6);
assert(inviteRoom.lastMessage == testMessage6);
@ -224,21 +224,22 @@ void test() async {
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
print('++++ ($testUserA) Restore user ++++');
FakeStore clientAStore = testClientA.storeAPI;
final clientADatabase = testClientA.database;
testClientA = null;
testClientA = Client('TestClient', debug: false);
testClientA.storeAPI = FakeStore(testClientA, clientAStore.storeMap);
testClientA.database = clientADatabase;
testClientA.connect();
await Future.delayed(Duration(seconds: 3));
var restoredRoom = testClientA.rooms.first;
assert(room != null);
assert(restoredRoom.id == room.id);
assert(restoredRoom.outboundGroupSession.session_id() ==
room.outboundGroupSession.session_id());
assert(restoredRoom.sessionKeys.length == 4);
assert(restoredRoom.sessionKeys.length == room.sessionKeys.length);
for (var i = 0; i < restoredRoom.sessionKeys.length; i++) {
assert(restoredRoom.sessionKeys.keys.toList()[i] ==
room.sessionKeys.keys.toList()[i]);
assert(restoredRoom.inboundGroupSessions.length == 4);
assert(restoredRoom.inboundGroupSessions.length == room.inboundGroupSessions.length);
for (var i = 0; i < restoredRoom.inboundGroupSessions.length; i++) {
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
room.inboundGroupSessions.keys.toList()[i]);
}
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
@ -253,7 +254,7 @@ void test() async {
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
testClientB.olmSessions[testClientA.identityKey].first.session_id());
/*assert(restoredRoom.outboundGroupSession.session_id() == currentSessionIdA);
assert(inviteRoom.sessionKeys
assert(inviteRoom.inboundGroupSessions
.containsKey(restoredRoom.outboundGroupSession.session_id()));*/
assert(restoredRoom.lastMessage == testMessage5);
assert(inviteRoom.lastMessage == testMessage5);