Merge branch 'soru/moor' into 'master'

Switch to moor

Closes #52

See merge request famedly/famedlysdk!284
This commit is contained in:
Christian Pauly 2020-05-15 18:40:17 +00:00
commit 98d2f8d6bb
27 changed files with 6645 additions and 658 deletions

1
.gitignore vendored
View file

@ -23,6 +23,7 @@ native/
**/doc/api/ **/doc/api/
.dart_tool/ .dart_tool/
.flutter-plugins .flutter-plugins
.flutter-plugins-dependencies
.packages .packages
.pub-cache/ .pub-cache/
.pub/ .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 - 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 install -y ./dart.deb
- apt update - 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/ - ln -s /usr/lib/dart/bin/pub /usr/bin/
- useradd -m test - useradd -m test
- chown -R 'test:' '.' - chown -R 'test:' '.'
@ -33,7 +33,7 @@ coverage_without_olm:
dependencies: [] dependencies: []
script: script:
- apt update - 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 - 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 install -y ./dart.deb
- ln -s /usr/lib/dart/bin/pub /usr/bin/ - 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/presence.dart';
export 'package:famedlysdk/src/room.dart'; export 'package:famedlysdk/src/room.dart';
export 'package:famedlysdk/src/room_account_data.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/timeline.dart';
export 'package:famedlysdk/src/user.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 'package:famedlysdk/famedlysdk.dart';
import './database/database.dart' show DbAccountData;
/// The global private data created by this user. /// The global private data created by this user.
class AccountData { class AccountData {
@ -38,4 +39,10 @@ class AccountData {
final content = Event.getMapFromPayload(jsonPayload['content']); final content = Event.getMapFromPayload(jsonPayload['content']);
return AccountData(content: content, typeKey: jsonPayload['type']); 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/account_data.dart';
import 'package:famedlysdk/src/presence.dart'; import 'package:famedlysdk/src/presence.dart';
import 'package:famedlysdk/src/room.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/sync/user_update.dart';
import 'package:famedlysdk/src/utils/device_keys_list.dart'; import 'package:famedlysdk/src/utils/device_keys_list.dart';
import 'package:famedlysdk/src/utils/matrix_file.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart';
@ -54,6 +53,7 @@ import 'sync/user_update.dart';
import 'user.dart'; import 'user.dart';
import 'utils/matrix_exception.dart'; import 'utils/matrix_exception.dart';
import 'utils/profile.dart'; import 'utils/profile.dart';
import 'database/database.dart' show Database;
import 'utils/pusher.dart'; import 'utils/pusher.dart';
typedef RoomSorter = int Function(Room a, Room b); typedef RoomSorter = int Function(Room a, Room b);
@ -70,12 +70,12 @@ class Client {
@deprecated @deprecated
Client get connection => this; Client get connection => this;
/// Optional persistent store for all data. int _id;
ExtendedStoreAPI get store => (storeAPI?.extended ?? false) ? storeAPI : null; 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) { onLoginStateChanged.stream.listen((loginState) {
print('LoginState: ${loginState.toString()}'); print('LoginState: ${loginState.toString()}');
}); });
@ -111,10 +111,6 @@ class Client {
String get deviceName => _deviceName; String get deviceName => _deviceName;
String _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. /// Returns the current login state.
bool isLogged() => accessToken != null; bool isLogged() => accessToken != null;
@ -208,7 +204,7 @@ class Client {
/// Checks the supported versions of the Matrix protocol and the supported /// Checks the supported versions of the Matrix protocol and the supported
/// login types. Returns false if the server is not compatible with the /// login types. Returns false if the server is not compatible with the
/// client. Automatically sets [matrixVersions]. /// client.
/// Throws FormatException, TimeoutException and MatrixException on error. /// Throws FormatException, TimeoutException and MatrixException on error.
Future<bool> checkServer(serverUrl) async { Future<bool> checkServer(serverUrl) async {
try { try {
@ -226,8 +222,6 @@ class Client {
} }
} }
_matrixVersions = versions;
final loginResp = final loginResp =
await jsonRequest(type: HTTPType.GET, action: '/client/r0/login'); await jsonRequest(type: HTTPType.GET, action: '/client/r0/login');
@ -243,7 +237,7 @@ class Client {
} }
return true; return true;
} catch (_) { } catch (_) {
_homeserver = _matrixVersions = null; _homeserver = null;
rethrow; rethrow;
} }
} }
@ -292,8 +286,7 @@ class Client {
newUserID: response['user_id'], newUserID: response['user_id'],
newHomeserver: homeserver, newHomeserver: homeserver,
newDeviceName: initialDeviceDisplayName ?? '', newDeviceName: initialDeviceDisplayName ?? '',
newDeviceID: response['device_id'], newDeviceID: response['device_id']);
newMatrixVersions: matrixVersions);
} }
return response; return response;
} }
@ -334,7 +327,6 @@ class Client {
newHomeserver: homeserver, newHomeserver: homeserver,
newDeviceName: initialDeviceDisplayName ?? '', newDeviceName: initialDeviceDisplayName ?? '',
newDeviceID: loginResp['device_id'], newDeviceID: loginResp['device_id'],
newMatrixVersions: matrixVersions,
); );
return true; return true;
} }
@ -676,20 +668,39 @@ class Client {
String newUserID, String newUserID,
String newDeviceName, String newDeviceName,
String newDeviceID, String newDeviceID,
List<String> newMatrixVersions,
String newPrevBatch, String newPrevBatch,
String newOlmAccount, String newOlmAccount,
}) async { }) async {
_accessToken = newToken; String olmAccount;
_homeserver = newHomeserver; if (database != null) {
_userID = newUserID; final account = await database.getClient(clientName);
_deviceID = newDeviceID; if (account != null) {
_deviceName = newDeviceName; _id = account.clientId;
_matrixVersions = newMatrixVersions; _homeserver = account.homeserverUrl;
prevBatch = newPrevBatch; _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. // Try to create a new olm account or restore a previous one.
if (newOlmAccount == null) { if (olmAccount == null) {
try { try {
await olm.init(); await olm.init();
_olmAccount = olm.Account(); _olmAccount = olm.Account();
@ -704,39 +715,30 @@ class Client {
try { try {
await olm.init(); await olm.init();
_olmAccount = olm.Account(); _olmAccount = olm.Account();
_olmAccount.unpickle(userID, newOlmAccount); _olmAccount.unpickle(userID, olmAccount);
} catch (_) { } catch (_) {
_olmAccount = null; _olmAccount = null;
} }
} }
if (storeAPI != null) { if (database != null) {
await storeAPI.storeClient(); if (id != null) {
_userDeviceKeys = await storeAPI.getUserDeviceKeys(); await database.updateClient(
final String olmSessionPickleString = _homeserver, _accessToken, _userID, _deviceID,
await storeAPI.getItem('/clients/$userID/olm-sessions'); _deviceName, prevBatch, pickledOlmAccount, id,
if (olmSessionPickleString != null) { );
final Map<String, dynamic> pickleMap = } else {
json.decode(olmSessionPickleString); _id = await database.insertClient(
for (var entry in pickleMap.entries) { clientName, _homeserver, _accessToken, _userID, _deviceID,
for (String pickle in entry.value) { _deviceName, prevBatch, pickledOlmAccount,
_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();
} }
_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); _userEventSub ??= onUserEvent.stream.listen(handleUserUpdate);
@ -755,14 +757,14 @@ class Client {
}); });
rooms.forEach((Room room) { rooms.forEach((Room room) {
room.clearOutboundGroupSession(wipe: true); room.clearOutboundGroupSession(wipe: true);
room.sessionKeys.values.forEach((SessionKey sessionKey) { room.inboundGroupSessions.values.forEach((SessionKey sessionKey) {
sessionKey.inboundGroupSession?.free(); sessionKey.inboundGroupSession?.free();
}); });
}); });
_olmAccount?.free(); _olmAccount?.free();
storeAPI?.clear(); database?.clear(id);
_accessToken = _homeserver = _id = _accessToken = _homeserver =
_userID = _deviceID = _deviceName = _matrixVersions = prevBatch = null; _userID = _deviceID = _deviceName = prevBatch = null;
_rooms = []; _rooms = [];
onLoginStateChanged.add(LoginState.loggedOut); onLoginStateChanged.add(LoginState.loggedOut);
} }
@ -858,6 +860,7 @@ class Client {
var exception = MatrixException(resp); var exception = MatrixException(resp);
if (exception.error == MatrixError.M_UNKNOWN_TOKEN) { if (exception.error == MatrixError.M_UNKNOWN_TOKEN) {
// The token is no longer valid. Need to sign off.... // The token is no longer valid. Need to sign off....
// TODO: add a way to export keys prior logout?
onError.add(exception); onError.add(exception);
clear(); clear();
} }
@ -926,14 +929,15 @@ class Client {
final syncResp = await _syncRequest; final syncResp = await _syncRequest;
if (hash != _syncRequest.hashCode) return; if (hash != _syncRequest.hashCode) return;
_timeoutFactor = 1; _timeoutFactor = 1;
if (store != null) { final futures = handleSync(syncResp);
await store.transaction(() { await database?.transaction(() async {
handleSync(syncResp); for (final f in futures) {
store.storePrevBatch(syncResp['next_batch']); await f();
}); }
} else { if (prevBatch != syncResp['next_batch']) {
await handleSync(syncResp); await database.storePrevBatch(syncResp['next_batch'], id);
} }
});
if (prevBatch == null) { if (prevBatch == null) {
onFirstSync.add(true); onFirstSync.add(true);
prevBatch = syncResp['next_batch']; prevBatch = syncResp['next_batch'];
@ -946,37 +950,39 @@ class Client {
onError.add(exception); onError.add(exception);
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync); await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
} catch (exception) { } catch (exception) {
print('Error during processing events: ' + exception.toString());
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync); await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
} }
} }
/// Use this method only for testing utilities! /// 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> && if (sync['to_device'] is Map<String, dynamic> &&
sync['to_device']['events'] is List<dynamic>) { sync['to_device']['events'] is List<dynamic>) {
_handleToDeviceEvents(sync['to_device']['events']); _handleToDeviceEvents(sync['to_device']['events']);
} }
if (sync['rooms'] is Map<String, dynamic>) { if (sync['rooms'] is Map<String, dynamic>) {
if (sync['rooms']['join'] 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>) { 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>) { 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> && if (sync['presence'] is Map<String, dynamic> &&
sync['presence']['events'] is List<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> && if (sync['account_data'] is Map<String, dynamic> &&
sync['account_data']['events'] is List<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>) { 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>) { if (sync['device_one_time_keys_count'] is Map<String, dynamic>) {
_handleDeviceOneTimeKeysCount(sync['device_one_time_keys_count']); _handleDeviceOneTimeKeysCount(sync['device_one_time_keys_count']);
@ -988,6 +994,7 @@ class Client {
); );
} }
onSync.add(sync); onSync.add(sync);
return dbActions;
} }
void _handleDeviceOneTimeKeysCount( 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) { if (deviceLists['changed'] is List) {
for (final userId in deviceLists['changed']) { for (final userId in deviceLists['changed']) {
if (_userDeviceKeys.containsKey(userId)) { if (_userDeviceKeys.containsKey(userId)) {
_userDeviceKeys[userId].outdated = true; _userDeviceKeys[userId].outdated = true;
if (database != null) {
dbActions.add(() => database.storeUserDeviceKeysInfo(id, userId, true));
}
} }
} }
for (final userId in deviceLists['left']) { for (final userId in deviceLists['left']) {
@ -1046,8 +1056,8 @@ class Client {
} }
} }
void _handleRooms(Map<String, dynamic> rooms, Membership membership) { void _handleRooms(Map<String, dynamic> rooms, Membership membership, List<Future<dynamic> Function()> dbActions) {
rooms.forEach((String id, dynamic room) async { rooms.forEach((String id, dynamic room) {
// calculate the notification counts, the limitedTimeline and prevbatch // calculate the notification counts, the limitedTimeline and prevbatch
num highlight_count = 0; num highlight_count = 0;
num notification_count = 0; num notification_count = 0;
@ -1089,40 +1099,55 @@ class Client {
summary: summary, summary: summary,
); );
_updateRoomsByRoomUpdate(update); _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); onRoomUpdate.add(update);
var handledEvents = false;
/// Handle now all room events and save them in the database /// Handle now all room events and save them in the database
if (room['state'] is Map<String, dynamic> && if (room['state'] is Map<String, dynamic> &&
room['state']['events'] is List<dynamic>) { room['state']['events'] is List<dynamic> &&
_handleRoomEvents(id, room['state']['events'], 'state'); room['state']['events'].isNotEmpty) {
_handleRoomEvents(id, room['state']['events'], 'state', dbActions);
handledEvents = true;
} }
if (room['invite_state'] is Map<String, dynamic> && if (room['invite_state'] is Map<String, dynamic> &&
room['invite_state']['events'] is List<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> && if (room['timeline'] is Map<String, dynamic> &&
room['timeline']['events'] is List<dynamic>) { room['timeline']['events'] is List<dynamic> &&
_handleRoomEvents(id, room['timeline']['events'], 'timeline'); room['timeline']['events'].isNotEmpty) {
_handleRoomEvents(id, room['timeline']['events'], 'timeline', dbActions);
handledEvents = true;
} }
if (room['ephemeral'] is Map<String, dynamic> && if (room['ephemeral'] is Map<String, dynamic> &&
room['ephemeral']['events'] is List<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> && if (room['account_data'] is Map<String, dynamic> &&
room['account_data']['events'] is List<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++) { 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 // Receipt events are deltas between two states. We will create a
// fake room account data event for this and store the difference // fake room account data event for this and store the difference
@ -1142,12 +1167,12 @@ class Client {
final mxid = userTimestampMapEntry.key; final mxid = userTimestampMapEntry.key;
// Remove previous receipt event from this user // Remove previous receipt event from this user
for (var entry in receiptStateContent.entries) { if (
if (entry.value['m.read'] is Map<String, dynamic> && receiptStateContent[eventID] is Map<String, dynamic> &&
entry.value['m.read'].containsKey(mxid)) { receiptStateContent[eventID]['m.read'] is Map<String, dynamic> &&
entry.value['m.read'].remove(mxid); receiptStateContent[eventID]['m.read'].containsKey(mxid)
break; ) {
} receiptStateContent[eventID]['m.read'].remove(mxid);
} }
if (userTimestampMap[mxid] is Map<String, dynamic> && if (userTimestampMap[mxid] is Map<String, dynamic> &&
userTimestampMap[mxid].containsKey('ts')) { userTimestampMap[mxid].containsKey('ts')) {
@ -1160,18 +1185,18 @@ class Client {
} }
} }
events[i]['content'] = receiptStateContent; 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++) { 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++) { for (var i = 0; i < events.length; i++) {
if (events[i]['type'] is String && if (events[i]['type'] is String &&
events[i]['content'] is Map<String, dynamic>) { events[i]['content'] is Map<String, dynamic>) {
@ -1180,42 +1205,51 @@ class Client {
type: type, type: type,
content: events[i], content: events[i],
); );
store?.storeUserEventUpdate(update); if (database != null) {
dbActions.add(() => database.storeUserEventUpdate(id, update));
}
onUserEvent.add(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>) { if (event['type'] is String && event['content'] is Map<String, dynamic>) {
// The client must ignore any new m.room.encryption event to prevent // The client must ignore any new m.room.encryption event to prevent
// man-in-the-middle attacks! // man-in-the-middle attacks!
if (event['type'] == 'm.room.encryption' && final room = getRoomById(roomID);
getRoomById(roomID).encrypted) { if (room == null || (event['type'] == 'm.room.encryption' &&
room.encrypted)) {
return; 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( var update = EventUpdate(
eventType: event['type'], eventType: event['type'],
roomID: roomID, roomID: roomID,
type: type, type: type,
content: event, content: event,
sortOrder: sortOrder,
); );
if (event['type'] == 'm.room.encrypted') { 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); _updateRoomsByEventUpdate(update);
onEvent.add(update); onEvent.add(update);
if (event['type'] == 'm.call.invite') { 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') { } 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') { } 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') { } 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' || if (eventUpdate.type == 'timeline' ||
eventUpdate.type == 'state' || eventUpdate.type == 'state' ||
eventUpdate.type == 'invite_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) { if (stateEvent.type == EventTypes.Redaction) {
final String redacts = eventUpdate.content['redacts']; final String redacts = eventUpdate.content['redacts'];
rooms[j].states.states.forEach( rooms[j].states.states.forEach(
@ -1337,6 +1371,7 @@ class Client {
var room = getRoomById(roomId); var room = getRoomById(roomId);
if (room == null && addToPendingIfNotFound) { if (room == null && addToPendingIfNotFound) {
_pendingToDeviceEvents.add(toDeviceEvent); _pendingToDeviceEvents.add(toDeviceEvent);
break;
} }
final String sessionId = toDeviceEvent.content['session_id']; final String sessionId = toDeviceEvent.content['session_id'];
if (toDeviceEvent.type == 'm.room_key' && if (toDeviceEvent.type == 'm.room_key' &&
@ -1349,7 +1384,7 @@ class Client {
.deviceKeys[toDeviceEvent.content['requesting_device_id']] .deviceKeys[toDeviceEvent.content['requesting_device_id']]
.ed25519Key; .ed25519Key;
} }
room.setSessionKey( room.setInboundGroupSession(
sessionId, sessionId,
toDeviceEvent.content, toDeviceEvent.content,
forwarded: toDeviceEvent.type == 'm.forwarded_room_key', forwarded: toDeviceEvent.type == 'm.forwarded_room_key',
@ -1379,7 +1414,7 @@ class Client {
.containsKey(toDeviceEvent.content['requesting_device_id'])) { .containsKey(toDeviceEvent.content['requesting_device_id'])) {
deviceKeys = userDeviceKeys[toDeviceEvent.sender] deviceKeys = userDeviceKeys[toDeviceEvent.sender]
.deviceKeys[toDeviceEvent.content['requesting_device_id']]; .deviceKeys[toDeviceEvent.content['requesting_device_id']];
if (room.sessionKeys.containsKey(sessionId)) { if (room.inboundGroupSessions.containsKey(sessionId)) {
final roomKeyRequest = final roomKeyRequest =
RoomKeyRequest.fromToDeviceEvent(toDeviceEvent, this); RoomKeyRequest.fromToDeviceEvent(toDeviceEvent, this);
if (deviceKeys.userId == userID && if (deviceKeys.userId == userID &&
@ -1446,6 +1481,7 @@ class Client {
Future<void> _updateUserDeviceKeys() async { Future<void> _updateUserDeviceKeys() async {
try { try {
if (!isLogged()) return; if (!isLogged()) return;
final dbActions = <Future<dynamic> Function()>[];
var trackedUserIds = await _getUserIdsInEncryptedRooms(); var trackedUserIds = await _getUserIdsInEncryptedRooms();
trackedUserIds.add(userID); trackedUserIds.add(userID);
@ -1481,6 +1517,7 @@ class Client {
final String deviceId = rawDeviceKeyEntry.key; final String deviceId = rawDeviceKeyEntry.key;
// Set the new device key for this device // Set the new device key for this device
if (!oldKeys.containsKey(deviceId)) { if (!oldKeys.containsKey(deviceId)) {
_userDeviceKeys[userId].deviceKeys[deviceId] = _userDeviceKeys[userId].deviceKeys[deviceId] =
DeviceKeys.fromJson(rawDeviceKeyEntry.value); DeviceKeys.fromJson(rawDeviceKeyEntry.value);
@ -1493,11 +1530,34 @@ class Client {
} else { } else {
_userDeviceKeys[userId].deviceKeys[deviceId] = oldKeys[deviceId]; _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; _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) { rooms.forEach((Room room) {
if (room.encrypted) { if (room.encrypted) {
room.clearOutboundGroupSession(); room.clearOutboundGroupSession();
@ -1616,7 +1676,7 @@ class Client {
oneTimeKeysCount) { oneTimeKeysCount) {
return false; return false;
} }
await storeAPI?.storeClient(); await database?.updateClientKeys(pickledOlmAccount, id);
lastTimeKeysUploaded = DateTime.now(); lastTimeKeysUploaded = DateTime.now();
return true; return true;
} }
@ -1668,7 +1728,7 @@ class Client {
var newSession = olm.Session(); var newSession = olm.Session();
newSession.create_inbound_from(_olmAccount, senderKey, body); newSession.create_inbound_from(_olmAccount, senderKey, body);
_olmAccount.remove_one_time_keys(newSession); _olmAccount.remove_one_time_keys(newSession);
storeAPI?.storeClient(); database?.updateClientKeys(pickledOlmAccount, id);
plaintext = newSession.decrypt(type, body); plaintext = newSession.decrypt(type, body);
storeOlmSession(senderKey, newSession); storeOlmSession(senderKey, newSession);
} }
@ -1695,29 +1755,24 @@ class Client {
/// A map from Curve25519 identity keys to existing olm sessions. /// A map from Curve25519 identity keys to existing olm sessions.
Map<String, List<olm.Session>> get olmSessions => _olmSessions; 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) { void storeOlmSession(String curve25519IdentityKey, olm.Session session) {
if (!_olmSessions.containsKey(curve25519IdentityKey)) { if (!_olmSessions.containsKey(curve25519IdentityKey)) {
_olmSessions[curve25519IdentityKey] = []; _olmSessions[curve25519IdentityKey] = [];
} }
if (_olmSessions[curve25519IdentityKey] final ix = _olmSessions[curve25519IdentityKey]
.indexWhere((s) => s.session_id() == session.session_id()) == .indexWhere((s) => s.session_id() == session.session_id());
if (ix ==
-1) { -1) {
// add a new session
_olmSessions[curve25519IdentityKey].add(session); _olmSessions[curve25519IdentityKey].add(session);
} else {
// update an existing session
_olmSessions[curve25519IdentityKey][ix] = session;
} }
var pickleMap = <String, List<String>>{}; final pickle = session.pickle(userID);
for (var entry in olmSessions.entries) { database?.storeOlmSession(id, curve25519IdentityKey, session.session_id(), pickle);
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));
} }
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send /// 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 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
import './room.dart'; import './room.dart';
import 'utils/matrix_localizations.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. /// 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 { class Event {
@ -96,6 +97,8 @@ class Event {
User get stateKeyUser => room.getUserByMXIDSync(stateKey); User get stateKeyUser => room.getUserByMXIDSync(stateKey);
double sortOrder;
Event( Event(
{this.status = defaultStatus, {this.status = defaultStatus,
this.content, this.content,
@ -107,7 +110,8 @@ class Event {
this.unsigned, this.unsigned,
this.prevContent, this.prevContent,
this.stateKey, this.stateKey,
this.room}); this.room,
this.sortOrder = 0.0});
static Map<String, dynamic> getMapFromPayload(dynamic payload) { static Map<String, dynamic> getMapFromPayload(dynamic payload) {
if (payload is String) { if (payload is String) {
@ -122,7 +126,7 @@ class Event {
} }
/// Get a State event from a table row or from the event stream. /// 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 content = Event.getMapFromPayload(jsonPayload['content']);
final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']); final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']);
final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']); final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']);
@ -140,6 +144,31 @@ class Event {
: DateTime.now(), : DateTime.now(),
unsigned: unsigned, unsigned: unsigned,
room: room, 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. /// from the database and the timelines. Returns false if not removed.
Future<bool> remove() async { Future<bool> remove() async {
if (status < 1) { if (status < 1) {
if (room.client.store != null) { await room.client.database?.removeEvent(room.client.id, eventId, room.id);
await room.client.store.removeEvent(eventId);
}
room.client.onEvent.add(EventUpdate( room.client.onEvent.add(EventUpdate(
roomID: room.id, roomID: room.id,
@ -349,7 +376,8 @@ class Event {
'event_id': eventId, 'event_id': eventId,
'status': -2, 'status': -2,
'content': {'body': 'Removed...'} 'content': {'body': 'Removed...'}
})); },
sortOrder: sortOrder));
return true; return true;
} }
return false; return false;
@ -470,13 +498,13 @@ class Event {
// Is this file storeable? // Is this file storeable?
final infoMap = final infoMap =
getThumbnail ? content['info']['thumbnail_info'] : content['info']; 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 is Map<String, dynamic> &&
infoMap['size'] is int && infoMap['size'] is int &&
infoMap['size'] <= room.client.store.maxFileSize; infoMap['size'] <= room.client.database.maxFileSize ;
if (storeable) { if (storeable) {
uint8list = await room.client.store.getFile(mxContent.toString()); uint8list = await room.client.database.getFile(mxContent.toString());
} }
// Download the file // Download the file
@ -484,7 +512,7 @@ class Event {
uint8list = uint8list =
(await http.get(mxContent.getDownloadLink(room.client))).bodyBytes; (await http.get(mxContent.getDownloadLink(room.client))).bodyBytes;
if (storeable) { 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/>. * 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 } enum PresenceType { online, offline, unavailable }
/// Informs the client of a user's presence state change. /// Informs the client of a user's presence state change.
@ -39,6 +42,8 @@ class Presence {
final String statusMsg; final String statusMsg;
final DateTime time; 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) Presence.fromJson(Map<String, dynamic> json)
: sender = json['sender'], : sender = json['sender'],
displayname = json['content']['displayname'], displayname = json['content']['displayname'],
@ -55,4 +60,23 @@ class Presence {
e.toString() == "PresenceType.${json['content']['presence']}", e.toString() == "PresenceType.${json['content']['presence']}",
orElse: () => null), orElse: () => null),
statusMsg = json['content']['status_msg']; 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/matrix_localizations.dart';
import 'utils/states_map.dart'; import 'utils/states_map.dart';
import './utils/markdown.dart'; import './utils/markdown.dart';
import './database/database.dart' show DbRoom;
enum PushRuleState { notify, mentions_only, dont_notify } enum PushRuleState { notify, mentions_only, dont_notify }
enum JoinRules { public, knock, invite, private } enum JoinRules { public, knock, invite, private }
@ -90,6 +91,27 @@ class Room {
List<String> _outboundGroupSessionDevices; 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 /// Clears the existing outboundGroupSession, tries to create a new one and
/// stores it as an ingoingGroupSession in the [sessionKeys]. Then sends the /// stores it as an ingoingGroupSession in the [sessionKeys]. Then sends the
/// new session encrypted with olm to all non-blocked devices using /// new session encrypted with olm to all non-blocked devices using
@ -120,7 +142,7 @@ class Room {
'session_id': outboundGroupSession.session_id(), 'session_id': outboundGroupSession.session_id(),
'session_key': outboundGroupSession.session_key(), 'session_key': outboundGroupSession.session_key(),
}; };
setSessionKey(rawSession['session_id'], rawSession); setInboundGroupSession(rawSession['session_id'], rawSession);
try { try {
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession); await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
_outboundGroupSession = outboundGroupSession; _outboundGroupSession = outboundGroupSession;
@ -137,12 +159,10 @@ class Room {
Future<void> _storeOutboundGroupSession() async { Future<void> _storeOutboundGroupSession() async {
if (_outboundGroupSession == null) return; if (_outboundGroupSession == null) return;
await client.storeAPI?.setItem( await client.database?.storeOutboundGroupSession(
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session', client.id, id, _outboundGroupSession.pickle(client.userID),
_outboundGroupSession.pickle(client.userID)); json.encode(_outboundGroupSessionDevices),
await client.storeAPI?.setItem( );
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session_devices',
json.encode(_outboundGroupSessionDevices));
return; return;
} }
@ -162,12 +182,11 @@ class Room {
return false; return false;
} }
} }
_outboundGroupSessionDevices == null; if (!wipe && _outboundGroupSessionDevices == null && _outboundGroupSession == null) {
await client.storeAPI?.setItem( return true; // let's just short-circuit out of here, no need to do DB stuff
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session', null); }
await client.storeAPI?.setItem( _outboundGroupSessionDevices = null;
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session_devices', await client.database?.removeOutboundGroupSession(client.id, id);
null);
_outboundGroupSession?.free(); _outboundGroupSession?.free();
_outboundGroupSession = null; _outboundGroupSession = null;
return true; return true;
@ -181,13 +200,13 @@ class Room {
/// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ", /// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
/// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..." /// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
/// } /// }
Map<String, SessionKey> get sessionKeys => _sessionKeys; Map<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions;
Map<String, SessionKey> _sessionKeys = {}; Map<String, SessionKey> _inboundGroupSessions = {};
/// Add a new session key to the [sessionKeys]. /// 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}) { {bool forwarded = false}) {
if (sessionKeys.containsKey(sessionId)) return; if (inboundGroupSessions.containsKey(sessionId)) return;
olm.InboundGroupSession inboundGroupSession; olm.InboundGroupSession inboundGroupSession;
if (content['algorithm'] == 'm.megolm.v1.aes-sha2') { if (content['algorithm'] == 'm.megolm.v1.aes-sha2') {
try { try {
@ -203,16 +222,17 @@ class Room {
e.toString()); e.toString());
} }
} }
_sessionKeys[sessionId] = SessionKey( _inboundGroupSessions[sessionId] = SessionKey(
content: content, content: content,
inboundGroupSession: inboundGroupSession, inboundGroupSession: inboundGroupSession,
indexes: {}, indexes: {},
key: client.userID, key: client.userID,
); );
if (_fullyRestored) { if (_fullyRestored) {
client.storeAPI?.setItem( client.database?.storeInboundGroupSession(client.id, id, sessionId,
'/clients/${client.deviceID}/rooms/${id}/session_keys', inboundGroupSession.pickle(client.userID), json.encode(content),
json.encode(sessionKeys)); json.encode({}),
);
} }
_tryAgainDecryptLastMessage(); _tryAgainDecryptLastMessage();
onSessionKeyReceived.add(sessionId); onSessionKeyReceived.add(sessionId);
@ -395,7 +415,9 @@ class Room {
this.mInvitedMemberCount = 0, this.mInvitedMemberCount = 0,
this.mJoinedMemberCount = 0, this.mJoinedMemberCount = 0,
this.roomAccountData = const {}, 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 /// The default count of how much events should be requested when requesting the
/// history of this room. /// history of this room.
@ -746,20 +768,22 @@ class Room {
}; };
} }
final sortOrder = newSortOrder;
// Display a *sending* event and store it. // Display a *sending* event and store it.
var eventUpdate = var eventUpdate = EventUpdate(type: 'timeline', roomID: id, eventType: type, sortOrder: sortOrder,
EventUpdate(type: 'timeline', roomID: id, eventType: type, content: { content: {
'type': type, 'type': type,
'event_id': messageID, 'event_id': messageID,
'sender': client.userID, 'sender': client.userID,
'status': 0, 'status': 0,
'origin_server_ts': now, 'origin_server_ts': now,
'content': content 'content': content
}); },
);
client.onEvent.add(eventUpdate); client.onEvent.add(eventUpdate);
await client.store?.transaction(() { await client.database?.transaction(() async {
client.store.storeEventUpdate(eventUpdate); await client.database.storeEventUpdate(client.id, eventUpdate);
return; await updateSortOrder();
}); });
// Send the text and on success, store and display a *sent* event. // Send the text and on success, store and display a *sent* event.
@ -767,7 +791,7 @@ class Room {
final response = await client.jsonRequest( final response = await client.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: '/client/r0/rooms/${id}/send/$sendType/$messageID', action: '/client/r0/rooms/${id}/send/$sendType/$messageID',
data: client.encryptionEnabled data: encrypted && client.encryptionEnabled
? await encryptGroupMessagePayload(content) ? await encryptGroupMessagePayload(content)
: content); : content);
final String res = response['event_id']; final String res = response['event_id'];
@ -775,9 +799,8 @@ class Room {
eventUpdate.content['unsigned'] = {'transaction_id': messageID}; eventUpdate.content['unsigned'] = {'transaction_id': messageID};
eventUpdate.content['event_id'] = res; eventUpdate.content['event_id'] = res;
client.onEvent.add(eventUpdate); client.onEvent.add(eventUpdate);
await client.store?.transaction(() { await client.database?.transaction(() async {
client.store.storeEventUpdate(eventUpdate); await client.database.storeEventUpdate(client.id, eventUpdate);
return;
}); });
return res; return res;
} catch (exception) { } catch (exception) {
@ -786,9 +809,8 @@ class Room {
eventUpdate.content['status'] = -1; eventUpdate.content['status'] = -1;
eventUpdate.content['unsigned'] = {'transaction_id': messageID}; eventUpdate.content['unsigned'] = {'transaction_id': messageID};
client.onEvent.add(eventUpdate); client.onEvent.add(eventUpdate);
await client.store?.transaction(() { await client.database?.transaction(() async {
client.store.storeEventUpdate(eventUpdate); await client.database.storeEventUpdate(client.id, eventUpdate);
return;
}); });
} }
return null; return null;
@ -809,7 +831,7 @@ class Room {
} }
} on MatrixException catch (exception) { } on MatrixException catch (exception) {
if (exception.errorMessage == 'No known servers') { if (exception.errorMessage == 'No known servers') {
await client.store?.forgetRoom(id); await client.database?.forgetRoom(client.id, id);
client.onRoomUpdate.add( client.onRoomUpdate.add(
RoomUpdate( RoomUpdate(
id: id, id: id,
@ -833,7 +855,7 @@ class Room {
/// Call the Matrix API to forget this room if you already left it. /// Call the Matrix API to forget this room if you already left it.
Future<void> forget() async { Future<void> forget() async {
await client.store?.forgetRoom(id); await client.database?.forgetRoom(client.id, id);
await client.jsonRequest( await client.jsonRequest(
type: HTTPType.POST, action: '/client/r0/rooms/${id}/forget'); type: HTTPType.POST, action: '/client/r0/rooms/${id}/forget');
return; return;
@ -903,65 +925,55 @@ class Room {
if (onHistoryReceived != null) onHistoryReceived(); if (onHistoryReceived != null) onHistoryReceived();
prev_batch = resp['end']; 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> && if (!(resp['chunk'] is List<dynamic> &&
resp['chunk'].length > 0 && resp['chunk'].length > 0 &&
resp['end'] is String)) return; resp['end'] is String)) return;
if (resp['state'] is List<dynamic>) { if (resp['state'] is List<dynamic>) {
await client.store?.transaction(() { for (final state in resp['state']) {
for (var i = 0; i < resp['state'].length; i++) { var eventUpdate = EventUpdate(
var eventUpdate = EventUpdate( type: 'state',
type: 'state', roomID: id,
roomID: id, eventType: state['type'],
eventType: resp['state'][i]['type'], content: state,
content: resp['state'][i], sortOrder: oldSortOrder,
).decrypt(this); ).decrypt(this);
client.onEvent.add(eventUpdate); client.onEvent.add(eventUpdate);
client.store.storeEventUpdate(eventUpdate); if (client.database != null) {
} dbActions.add(() => client.database.storeEventUpdate(client.id, 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);
} }
} }
} }
List<dynamic> history = resp['chunk']; List<dynamic> history = resp['chunk'];
await client.store?.transaction(() { for (final hist in history) {
for (var i = 0; i < history.length; i++) { var eventUpdate = EventUpdate(
var eventUpdate = EventUpdate( type: 'history',
type: 'history', roomID: id,
roomID: id, eventType: hist['type'],
eventType: history[i]['type'], content: hist,
content: history[i], sortOrder: oldSortOrder,
).decrypt(this); ).decrypt(this);
client.onEvent.add(eventUpdate); client.onEvent.add(eventUpdate);
client.store.storeEventUpdate(eventUpdate); if (client.database != null) {
client.store.setRoomPrevBatch(id, resp['end']); dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate));
}
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);
} }
} }
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( client.onRoomUpdate.add(
RoomUpdate( RoomUpdate(
id: id, id: id,
@ -1013,7 +1025,7 @@ class Room {
/// Sends *m.fully_read* and *m.read* for the given event ID. /// Sends *m.fully_read* and *m.read* for the given event ID.
Future<void> sendReadReceipt(String eventID) async { Future<void> sendReadReceipt(String eventID) async {
notificationCount = 0; notificationCount = 0;
await client?.store?.resetNotificationCount(id); await client.database?.resetNotificationCount(client.id, id);
await client.jsonRequest( await client.jsonRequest(
type: HTTPType.POST, type: HTTPType.POST,
action: '/client/r0/rooms/$id/read_markers', action: '/client/r0/rooms/$id/read_markers',
@ -1024,48 +1036,44 @@ class Room {
return; 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 // Restore the inbound and outbound session keys
if (client.encryptionEnabled && client.storeAPI != null) { if (client.encryptionEnabled && client.database != null) {
final String outboundGroupSessionPickle = await client.storeAPI.getItem( outboundGroupSession ??= client.database.getDbOutboundGroupSession(client.id, id);
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session'); inboundGroupSessions ??= client.database.getDbInboundGroupSessions(client.id, id);
if (outboundGroupSessionPickle != null) { if (outboundGroupSession is Future) {
outboundGroupSession = await outboundGroupSession;
}
if (inboundGroupSessions is Future) {
inboundGroupSessions = await inboundGroupSessions;
}
if (outboundGroupSession != false && outboundGroupSession != null) {
try { try {
_outboundGroupSession = olm.OutboundGroupSession(); _outboundGroupSession = olm.OutboundGroupSession();
_outboundGroupSession.unpickle( _outboundGroupSession.unpickle(
client.userID, outboundGroupSessionPickle); client.userID, outboundGroupSession.pickle);
} catch (e) { } catch (e) {
_outboundGroupSession = null; _outboundGroupSession = null;
print('[LibOlm] Unable to unpickle outboundGroupSession: ' + print('[LibOlm] Unable to unpickle outboundGroupSession: ' +
e.toString()); e.toString());
} }
}
final String outboundGroupSessionDevicesString = await client.storeAPI
.getItem(
'/clients/${client.deviceID}/rooms/${id}/outbound_group_session_devices');
if (outboundGroupSessionDevicesString != null) {
_outboundGroupSessionDevices = _outboundGroupSessionDevices =
List<String>.from(json.decode(outboundGroupSessionDevicesString)); List<String>.from(json.decode(outboundGroupSession.deviceIds));
} }
final String sessionKeysPickle = await client.storeAPI if (inboundGroupSessions?.isNotEmpty ?? false) {
.getItem('/clients/${client.deviceID}/rooms/${id}/session_keys'); _inboundGroupSessions ??= {};
if (sessionKeysPickle?.isNotEmpty ?? false) { for (final sessionKey in inboundGroupSessions) {
final Map<String, dynamic> map = json.decode(sessionKeysPickle);
_sessionKeys ??= {};
for (var entry in map.entries) {
try { try {
_sessionKeys[entry.key] = _inboundGroupSessions[sessionKey.sessionId] = SessionKey.fromDb(sessionKey, client.userID);
SessionKey.fromJson(entry.value, client.userID);
} catch (e) { } catch (e) {
print('[LibOlm] Could not unpickle inboundGroupSession: ' + print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
e.toString());
} }
} }
} }
} }
await client.storeAPI?.setItem(
'/clients/${client.deviceID}/rooms/${id}/session_keys',
json.encode(sessionKeys));
_tryAgainDecryptLastMessage(); _tryAgainDecryptLastMessage();
_fullyRestored = true; _fullyRestored = true;
return; return;
@ -1076,44 +1084,64 @@ class Room {
/// Returns a Room from a json String which comes normally from the store. If the /// Returns a Room from a json String which comes normally from the store. If the
/// state are also given, the method will await them. /// state are also given, the method will await them.
static Future<Room> getRoomFromTableRow( static Future<Room> getRoomFromTableRow(
Map<String, dynamic> row, Client matrix, DbRoom row, // either Map<String, dynamic> or DbRoom
{Future<List<Map<String, dynamic>>> states, Client matrix,
Future<List<Map<String, dynamic>>> roomAccountData}) async { {
var newRoom = Room( dynamic states, // DbRoomState, as iterator and optionally as future
id: row['room_id'], 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 membership: Membership.values
.firstWhere((e) => e.toString() == 'Membership.' + row['membership']), .firstWhere((e) => e.toString() == 'Membership.' + row.membership),
notificationCount: row['notification_count'], notificationCount: row.notificationCount,
highlightCount: row['highlight_count'], highlightCount: row.highlightCount,
notificationSettings: row['notification_settings'], notificationSettings: 'mention', // TODO: do proper things
prev_batch: row['prev_batch'], prev_batch: row.prevBatch,
mInvitedMemberCount: row['invited_member_count'], mInvitedMemberCount: row.invitedMemberCount,
mJoinedMemberCount: row['joined_member_count'], mJoinedMemberCount: row.joinedMemberCount,
mHeroes: row['heroes']?.split(',') ?? [], mHeroes: row.heroes?.split(',') ?? [],
client: matrix, client: matrix,
roomAccountData: {}, roomAccountData: {},
newestSortOrder: row.newestSortOrder,
oldestSortOrder: row.oldestSortOrder,
); );
// Restore the inbound and outbound session keys
await newRoom.restoreGroupSessionKeys();
if (states != null) { if (states != null) {
var rawStates = await states; var rawStates;
for (var i = 0; i < rawStates.length; i++) { if (states is Future) {
var newState = Event.fromJson(rawStates[i], newRoom); rawStates = await states;
} else {
rawStates = states;
}
for (final rawState in rawStates) {
final newState = Event.fromDb(rawState, newRoom);;
newRoom.setState(newState); newRoom.setState(newState);
} }
} }
var newRoomAccountData = <String, RoomAccountData>{}; var newRoomAccountData = <String, RoomAccountData>{};
if (roomAccountData != null) { if (roomAccountData != null) {
var rawRoomAccountData = await roomAccountData; var rawRoomAccountData;
for (var i = 0; i < rawRoomAccountData.length; i++) { if (roomAccountData is Future) {
var newData = RoomAccountData.fromJson(rawRoomAccountData[i], newRoom); rawRoomAccountData = await roomAccountData;
} else {
rawRoomAccountData = roomAccountData;
}
for (final singleAccountData in rawRoomAccountData) {
final newData = RoomAccountData.fromDb(singleAccountData, newRoom);
newRoomAccountData[newData.typeKey] = newData; 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; return newRoom;
} }
@ -1122,24 +1150,28 @@ class Room {
Future<Timeline> getTimeline( Future<Timeline> getTimeline(
{onTimelineUpdateCallback onUpdate, {onTimelineUpdateCallback onUpdate,
onTimelineInsertCallback onInsert}) async { onTimelineInsertCallback onInsert}) async {
var events = client.store != null var events;
? await client.store.getEventList(this) if (client.database != null) {
: <Event>[]; events = await client.database.getEventList(client.id, this);
} else {
events = <Event>[];
}
// Try again to decrypt encrypted events and update the database. // Try again to decrypt encrypted events and update the database.
if (encrypted && client.store != null) { if (encrypted && client.database != null) {
await client.store.transaction(() { await client.database.transaction(() async {
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted && if (events[i].type == EventTypes.Encrypted &&
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) { events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
events[i] = events[i].decrypted; events[i] = events[i].decrypted;
if (events[i].type != EventTypes.Encrypted) { if (events[i].type != EventTypes.Encrypted) {
client.store.storeEventUpdate( await client.database.storeEventUpdate(client.id,
EventUpdate( EventUpdate(
eventType: events[i].typeKey, eventType: events[i].typeKey,
content: events[i].toJson(), content: events[i].toJson(),
roomID: events[i].roomId, roomID: events[i].roomId,
type: 'timeline', type: 'timeline',
sortOrder: events[i].sortOrder,
), ),
); );
} }
@ -1154,7 +1186,7 @@ class Room {
onUpdate: onUpdate, onUpdate: onUpdate,
onInsert: onInsert, onInsert: onInsert,
); );
if (client.store == null) { if (client.database == null) {
prev_batch = ''; prev_batch = '';
await requestHistory(historyCount: 10); await requestHistory(historyCount: 10);
} }
@ -1227,6 +1259,9 @@ class Room {
/// Requests a missing [User] for this room. Important for clients using /// Requests a missing [User] for this room. Important for clients using
/// lazy loading. /// lazy loading.
Future<User> requestUser(String mxID, {bool ignoreErrors = false}) async { 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; if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
Map<String, dynamic> resp; Map<String, dynamic> resp;
try { try {
@ -1237,23 +1272,30 @@ class Room {
_requestingMatrixIds.remove(mxID); _requestingMatrixIds.remove(mxID);
if (!ignoreErrors) rethrow; if (!ignoreErrors) rethrow;
} }
if (resp == null) {
return null;
}
final user = User(mxID, final user = User(mxID,
displayName: resp['displayname'], displayName: resp['displayname'],
avatarUrl: resp['avatar_url'], avatarUrl: resp['avatar_url'],
room: this); room: this);
states[mxID] = user; states[mxID] = user;
if (client.store != null) { await client.database?.transaction(() async {
await client.store.transaction(() { final content = <String, dynamic>{
client.store.storeEventUpdate( 'sender': mxID,
EventUpdate( 'type': 'm.room.member',
content: resp, 'content': resp,
roomID: id, 'state_key': mxID,
type: 'state', };
eventType: 'm.room.member'), await client.database.storeEventUpdate(client.id,
); EventUpdate(
return; content: content,
}); roomID: id,
} type: 'state',
eventType: 'm.room.member',
sortOrder: 0.0),
);
});
if (onUpdate != null) onUpdate.add(id); if (onUpdate != null) onUpdate.add(id);
_requestingMatrixIds.remove(mxID); _requestingMatrixIds.remove(mxID);
return user; return user;
@ -1690,12 +1732,11 @@ class Room {
Future<List<DeviceKeys>> getUserDeviceKeys() async { Future<List<DeviceKeys>> getUserDeviceKeys() async {
var deviceKeys = <DeviceKeys>[]; var deviceKeys = <DeviceKeys>[];
var users = await requestParticipants(); var users = await requestParticipants();
for (final userDeviceKeyEntry in client.userDeviceKeys.entries) { for (final user in users) {
if (users.indexWhere((u) => u.id == userDeviceKeyEntry.key) == -1) { if (client.userDeviceKeys.containsKey(user.id)) {
continue; for (var deviceKeyEntry in client.userDeviceKeys[user.id].deviceKeys.values) {
} deviceKeys.add(deviceKeyEntry);
for (var deviceKeyEntry in userDeviceKeyEntry.value.deviceKeys.values) { }
deviceKeys.add(deviceKeyEntry);
} }
} }
return deviceKeys; return deviceKeys;
@ -1745,23 +1786,23 @@ class Room {
throw (DecryptError.UNKNOWN_ALGORITHM); throw (DecryptError.UNKNOWN_ALGORITHM);
} }
final String sessionId = event.content['session_id']; final String sessionId = event.content['session_id'];
if (!sessionKeys.containsKey(sessionId)) { if (!inboundGroupSessions.containsKey(sessionId)) {
throw (DecryptError.UNKNOWN_SESSION); throw (DecryptError.UNKNOWN_SESSION);
} }
final decryptResult = sessionKeys[sessionId] final decryptResult = inboundGroupSessions[sessionId]
.inboundGroupSession .inboundGroupSession
.decrypt(event.content['ciphertext']); .decrypt(event.content['ciphertext']);
final messageIndexKey = final messageIndexKey =
event.eventId + event.time.millisecondsSinceEpoch.toString(); event.eventId + event.time.millisecondsSinceEpoch.toString();
if (sessionKeys[sessionId].indexes.containsKey(messageIndexKey) && if (inboundGroupSessions[sessionId].indexes.containsKey(messageIndexKey) &&
sessionKeys[sessionId].indexes[messageIndexKey] != inboundGroupSessions[sessionId].indexes[messageIndexKey] !=
decryptResult.message_index) { decryptResult.message_index) {
if ((_outboundGroupSession?.session_id() ?? '') == sessionId) { if ((_outboundGroupSession?.session_id() ?? '') == sessionId) {
clearOutboundGroupSession(); clearOutboundGroupSession();
} }
throw (DecryptError.CHANNEL_CORRUPTED); throw (DecryptError.CHANNEL_CORRUPTED);
} }
sessionKeys[sessionId].indexes[messageIndexKey] = inboundGroupSessions[sessionId].indexes[messageIndexKey] =
decryptResult.message_index; decryptResult.message_index;
_storeOutboundGroupSession(); _storeOutboundGroupSession();
decryptedPayload = json.decode(decryptResult.plaintext); decryptedPayload = json.decode(decryptResult.plaintext);
@ -1799,6 +1840,7 @@ class Room {
stateKey: event.stateKey, stateKey: event.stateKey,
prevContent: event.prevContent, prevContent: event.prevContent,
status: event.status, status: event.status,
sortOrder: event.sortOrder,
); );
} }
} }

View file

@ -24,6 +24,7 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/account_data.dart'; import 'package:famedlysdk/src/account_data.dart';
import 'package:famedlysdk/src/event.dart'; import 'package:famedlysdk/src/event.dart';
import './database/database.dart' show DbRoomAccountData;
/// Stripped down events for account data and ephemrals of a room. /// Stripped down events for account data and ephemrals of a room.
class RoomAccountData extends AccountData { class RoomAccountData extends AccountData {
@ -46,4 +47,14 @@ class RoomAccountData extends AccountData {
roomId: jsonPayload['room_id'], roomId: jsonPayload['room_id'],
room: room); 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. // The json payload of the content of this event.
final Map<String, dynamic> content; 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) { EventUpdate decrypt(Room room) {
if (eventType != 'm.room.encrypted') { if (eventType != 'm.room.encrypted') {
@ -48,12 +51,13 @@ class EventUpdate {
} }
try { try {
var decrpytedEvent = var decrpytedEvent =
room.decryptGroupMessage(Event.fromJson(content, room)); room.decryptGroupMessage(Event.fromJson(content, room, sortOrder));
return EventUpdate( return EventUpdate(
eventType: eventType, eventType: eventType,
roomID: roomID, roomID: roomID,
type: type, type: type,
content: decrpytedEvent.toJson(), content: decrpytedEvent.toJson(),
sortOrder: sortOrder,
); );
} catch (e) { } catch (e) {
print('[LibOlm] Could not decrypt megolm event: ' + e.toString()); 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']); final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
if (eventId != null) { if (eventId != null) {
events[eventId] events[eventId]
.setRedactionEvent(Event.fromJson(eventUpdate.content, room)); .setRedactionEvent(Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder));
} }
} else if (eventUpdate.content['status'] == -2) { } else if (eventUpdate.content['status'] == -2) {
var i = _findEvent(event_id: eventUpdate.content['event_id']); var i = _findEvent(event_id: eventUpdate.content['event_id']);
@ -138,18 +138,18 @@ class Timeline {
: null); : null);
if (i < events.length) { if (i < events.length) {
events[i] = Event.fromJson(eventUpdate.content, room); events[i] = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
} }
} else { } else {
Event newEvent; Event newEvent;
var senderUser = await room.client.store var senderUser = room.getState('m.room.member', eventUpdate.content['sender'])?.asUser ?? await room.client.database
?.getUser(matrixID: eventUpdate.content['sender'], room: room); ?.getUser(room.client.id, eventUpdate.content['sender'], room);
if (senderUser != null) { if (senderUser != null) {
eventUpdate.content['displayname'] = senderUser.displayName; eventUpdate.content['displayname'] = senderUser.displayName;
eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString(); 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' && if (eventUpdate.type == 'history' &&
events.indexWhere( events.indexWhere(
@ -173,8 +173,7 @@ class Timeline {
void sort() { void sort() {
if (sortLock || events.length < 2) return; if (sortLock || events.length < 2) return;
sortLock = true; sortLock = true;
events?.sort((a, b) => events?.sort((a, b) => b.sortOrder - a.sortOrder > 0 ? 1 : -1);
b.time.millisecondsSinceEpoch.compareTo(a.time.millisecondsSinceEpoch));
sortLock = false; sortLock = false;
} }

View file

@ -1,12 +1,23 @@
import 'dart:convert'; import 'dart:convert';
import '../client.dart'; import '../client.dart';
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
import '../event.dart';
class DeviceKeysList { class DeviceKeysList {
String userId; String userId;
bool outdated = true; bool outdated = true;
Map<String, DeviceKeys> deviceKeys = {}; 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) { DeviceKeysList.fromJson(Map<String, dynamic> json) {
userId = json['user_id']; userId = json['user_id'];
outdated = json['outdated']; outdated = json['outdated'];
@ -52,7 +63,7 @@ class DeviceKeys {
Future<void> setVerified(bool newVerified, Client client) { Future<void> setVerified(bool newVerified, Client client) {
verified = newVerified; verified = newVerified;
return client.storeAPI.storeUserDeviceKeys(client.userDeviceKeys); return client.database?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
} }
Future<void> setBlocked(bool newBlocked, Client client) { Future<void> setBlocked(bool newBlocked, Client client) {
@ -63,7 +74,7 @@ class DeviceKeys {
room.clearOutboundGroupSession(); room.clearOutboundGroupSession();
} }
} }
return client.storeAPI.storeUserDeviceKeys(client.userDeviceKeys); return client.database?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
} }
DeviceKeys({ DeviceKeys({
@ -77,6 +88,22 @@ class DeviceKeys {
this.blocked, 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) { DeviceKeys.fromJson(Map<String, dynamic> json) {
userId = json['user_id']; userId = json['user_id'];
deviceId = json['device_id']; deviceId = json['device_id'];

View file

@ -16,7 +16,7 @@ class RoomKeyRequest extends ToDeviceEvent {
Future<void> forwardKey() async { Future<void> forwardKey() async {
var room = this.room; 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]; var forwardedKeys = <dynamic>[client.identityKey];
for (final key in session.forwardingCurve25519KeyChain) { for (final key in session.forwardingCurve25519KeyChain) {
forwardedKeys.add(key); forwardedKeys.add(key);

View file

@ -2,6 +2,9 @@ import 'dart:convert';
import 'package:olm/olm.dart'; import 'package:olm/olm.dart';
import '../database/database.dart' show DbInboundGroupSession;
import '../event.dart';
class SessionKey { class SessionKey {
Map<String, dynamic> content; Map<String, dynamic> content;
Map<String, int> indexes; Map<String, int> indexes;
@ -14,6 +17,20 @@ class SessionKey {
SessionKey({this.content, this.inboundGroupSession, this.key, this.indexes}); 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 { SessionKey.fromJson(Map<String, dynamic> json, String key) : key = key {
content = json['content'] != null content = json['content'] != null
? Map<String, dynamic>.from(json['content']) ? Map<String, dynamic>.from(json['content'])

View file

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

View file

@ -14,6 +14,7 @@ dependencies:
image: ^2.1.4 image: ^2.1.4
markdown: ^2.1.3 markdown: ^2.1.3
html_unescape: ^1.0.1+3 html_unescape: ^1.0.1+3
moor: ^3.0.2
olm: olm:
git: git:
@ -27,5 +28,7 @@ dependencies:
dev_dependencies: dev_dependencies:
test: ^1.0.0 test: ^1.0.0
moor_generator: ^3.0.0
build_runner: ^1.5.2 build_runner: ^1.5.2
pedantic: ^1.9.0 # DO NOT UPDATE AS THIS WOULD CAUSE FLUTTER TO FAIL 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 'package:test/test.dart';
import 'fake_matrix_api.dart'; import 'fake_matrix_api.dart';
import 'fake_store.dart'; import 'fake_database.dart';
void main() { void main() {
Client matrix; Client matrix;
@ -86,7 +86,6 @@ void main() {
}); });
expect(matrix.homeserver, null); expect(matrix.homeserver, null);
expect(matrix.matrixVersions, null);
try { try {
await matrix.checkServer('https://fakeserver.wrongaddress'); await matrix.checkServer('https://fakeserver.wrongaddress');
@ -95,8 +94,6 @@ void main() {
} }
await matrix.checkServer('https://fakeserver.notexisting'); await matrix.checkServer('https://fakeserver.notexisting');
expect(matrix.homeserver, '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 final resp = await matrix
.jsonRequest(type: HTTPType.POST, action: '/client/r0/login', data: { .jsonRequest(type: HTTPType.POST, action: '/client/r0/login', data: {
@ -128,7 +125,6 @@ void main() {
newHomeserver: matrix.homeserver, newHomeserver: matrix.homeserver,
newDeviceName: 'Text Matrix Client', newDeviceName: 'Text Matrix Client',
newDeviceID: resp['device_id'], newDeviceID: resp['device_id'],
newMatrixVersions: matrix.matrixVersions,
newOlmAccount: pickledOlmAccount, newOlmAccount: pickledOlmAccount,
); );
@ -160,18 +156,18 @@ void main() {
expect(matrix.directChats, matrix.accountData['m.direct'].content); expect(matrix.directChats, matrix.accountData['m.direct'].content);
expect(matrix.presences.length, 1); expect(matrix.presences.length, 1);
expect(matrix.rooms[1].ephemerals.length, 2); expect(matrix.rooms[1].ephemerals.length, 2);
expect(matrix.rooms[1].sessionKeys.length, 1); expect(matrix.rooms[1].inboundGroupSessions.length, 1);
expect( expect(
matrix matrix
.rooms[1] .rooms[1]
.sessionKeys['ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'] .inboundGroupSessions['ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
.content['session_key'], .content['session_key'],
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'); 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw');
if (olmEnabled) { if (olmEnabled) {
expect( expect(
matrix matrix
.rooms[1] .rooms[1]
.sessionKeys['ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'] .inboundGroupSessions['ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
.inboundGroupSession != .inboundGroupSession !=
null, null,
true); true);
@ -279,7 +275,6 @@ void main() {
expect(matrix.userID == null, true); expect(matrix.userID == null, true);
expect(matrix.deviceID == null, true); expect(matrix.deviceID == null, true);
expect(matrix.deviceName == null, true); expect(matrix.deviceName == null, true);
expect(matrix.matrixVersions == null, true);
expect(matrix.prevBatch == null, true); expect(matrix.prevBatch == null, true);
var loginState = await loginStateFuture; var loginState = await loginStateFuture;
@ -635,8 +630,7 @@ void main() {
test('Test the fake store api', () async { test('Test the fake store api', () async {
var client1 = Client('testclient', debug: true); var client1 = Client('testclient', debug: true);
client1.httpClient = FakeMatrixApi(); client1.httpClient = FakeMatrixApi();
var fakeStore = FakeStore(client1, {}); client1.database = getDatabase();
client1.storeAPI = fakeStore;
client1.connect( client1.connect(
newToken: 'abc123', newToken: 'abc123',
@ -644,14 +638,6 @@ void main() {
newHomeserver: 'https://fakeServer.notExisting', newHomeserver: 'https://fakeServer.notExisting',
newDeviceName: 'Text Matrix Client', newDeviceName: 'Text Matrix Client',
newDeviceID: 'GHTYAJCE', newDeviceID: 'GHTYAJCE',
newMatrixVersions: [
'r0.0.1',
'r0.1.0',
'r0.2.0',
'r0.3.0',
'r0.4.0',
'r0.5.0'
],
newOlmAccount: pickledOlmAccount, newOlmAccount: pickledOlmAccount,
); );
@ -669,8 +655,9 @@ void main() {
var client2 = Client('testclient', debug: true); var client2 = Client('testclient', debug: true);
client2.httpClient = FakeMatrixApi(); client2.httpClient = FakeMatrixApi();
client2.storeAPI = FakeStore(client2, fakeStore.storeMap); client2.database = client1.database;
client2.connect();
await Future.delayed(Duration(milliseconds: 100)); await Future.delayed(Duration(milliseconds: 100));
expect(client2.isLogged(), true); expect(client2.isLogged(), true);
@ -679,11 +666,10 @@ void main() {
expect(client2.homeserver, client1.homeserver); expect(client2.homeserver, client1.homeserver);
expect(client2.deviceID, client1.deviceID); expect(client2.deviceID, client1.deviceID);
expect(client2.deviceName, client1.deviceName); expect(client2.deviceName, client1.deviceName);
expect(client2.matrixVersions, client1.matrixVersions);
if (client2.encryptionEnabled) { if (client2.encryptionEnabled) {
expect(client2.pickledOlmAccount, client1.pickledOlmAccount); expect(client2.pickledOlmAccount, client1.pickledOlmAccount);
expect(json.encode(client2.rooms[1].sessionKeys[sessionKey]), expect(json.encode(client2.rooms[1].inboundGroupSessions[sessionKey]),
json.encode(client1.rooms[1].sessionKeys[sessionKey])); json.encode(client1.rooms[1].inboundGroupSessions[sessionKey]));
expect(client2.rooms[1].id, client1.rooms[1].id); expect(client2.rooms[1].id, client1.rooms[1].id);
expect(client2.rooms[1].outboundGroupSession.session_key(), sessionKey); 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 'package:test/test.dart';
import 'fake_matrix_api.dart'; import 'fake_matrix_api.dart';
import 'fake_store.dart'; import 'fake_database.dart';
void main() { void main() {
/// All Tests related to device keys /// All Tests related to device keys
@ -53,13 +53,13 @@ void main() {
var matrix = Client('testclient', debug: true); var matrix = Client('testclient', debug: true);
matrix.httpClient = FakeMatrixApi(); matrix.httpClient = FakeMatrixApi();
matrix.storeAPI = FakeStore(matrix, {}); matrix.database = getDatabase();
await matrix.checkServer('https://fakeServer.notExisting'); await matrix.checkServer('https://fakeServer.notExisting');
await matrix.login('test', '1234'); await matrix.login('test', '1234');
var room = matrix.getRoomById('!726s6s6q:example.com'); var room = matrix.getRoomById('!726s6s6q:example.com');
if (matrix.encryptionEnabled) { if (matrix.encryptionEnabled) {
await room.createOutboundGroupSession(); 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( var roomKeyRequest = RoomKeyRequest.fromToDeviceEvent(
ToDeviceEvent.fromJson(rawJson), matrix); 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/room.dart';
import 'package:famedlysdk/src/user.dart'; import 'package:famedlysdk/src/user.dart';
import 'package:famedlysdk/src/utils/matrix_file.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 'package:test/test.dart';
import 'fake_matrix_api.dart'; import 'fake_matrix_api.dart';
@ -62,44 +63,50 @@ void main() {
'@charley:example.org' '@charley:example.org'
]; ];
var jsonObj = <String, dynamic>{ var dbRoom = DbRoom(
'room_id': id, clientId: 1,
'membership': membership.toString().split('.').last, roomId: id,
'avatar_url': '', membership: membership.toString().split('.').last,
'notification_count': notificationCount, highlightCount: highlightCount,
'highlight_count': highlightCount, notificationCount: notificationCount,
'prev_batch': '', prevBatch: '',
'joined_member_count': notificationCount, joinedMemberCount: notificationCount,
'invited_member_count': notificationCount, invitedMemberCount: notificationCount,
'heroes': heroes.join(','), newestSortOrder: 0.0,
}; oldestSortOrder: 0.0,
heroes: heroes.join(','),
);
Function states = () async => [ var states = [
{ DbRoomState(
'content': {'join_rule': 'public'}, clientId: 1,
'event_id': '143273582443PhrSn:example.org', eventId: '143273582443PhrSn:example.org',
'origin_server_ts': 1432735824653, roomId: id,
'room_id': id, sortOrder: 0.0,
'sender': '@example:example.org', originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653),
'state_key': '', sender: '@example:example.org',
'type': 'm.room.join_rules', type: 'm.room.join_rules',
'unsigned': {'age': 1234} unsigned: '{"age": 1234}',
} content: '{"join_rule": "public"}',
]; prevContent: '',
stateKey: '',
),
];
Function roomAccountData = () async => [ var roomAccountData = [
{ DbRoomAccountData(
'content': {'foo': 'bar'}, clientId: 1,
'room_id': id, type: 'com.test.foo',
'type': 'com.test.foo' roomId: id,
} content: '{"foo": "bar"}',
]; ),
];
room = await Room.getRoomFromTableRow( room = await Room.getRoomFromTableRow(
jsonObj, dbRoom,
matrix, matrix,
states: states(), states: states,
roomAccountData: roomAccountData(), roomAccountData: roomAccountData,
); );
expect(room.id, id); expect(room.id, id);
@ -390,14 +397,14 @@ void main() {
expect(room.outboundGroupSession != null, true); expect(room.outboundGroupSession != null, true);
expect(room.outboundGroupSession.session_id().isNotEmpty, true); expect(room.outboundGroupSession.session_id().isNotEmpty, true);
expect( expect(
room.sessionKeys.containsKey(room.outboundGroupSession.session_id()), room.inboundGroupSessions.containsKey(room.outboundGroupSession.session_id()),
true); true);
expect( expect(
room.sessionKeys[room.outboundGroupSession.session_id()] room.inboundGroupSessions[room.outboundGroupSession.session_id()]
.content['session_key'], .content['session_key'],
room.outboundGroupSession.session_key()); room.outboundGroupSession.session_key());
expect( expect(
room.sessionKeys[room.outboundGroupSession.session_id()].indexes room.inboundGroupSessions[room.outboundGroupSession.session_id()].indexes
.length, .length,
0); 0);
}); });

View file

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

View file

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