diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6154c0f..f9bcfa4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -133,14 +133,14 @@ pages: # Adds license-scanning job. Because Gitlab does not support pub.dev # we added https://github.com/oss-review-toolkit/ort -include: - - template: Security/License-Scanning.gitlab-ci.yml +#include: +# - template: Security/License-Scanning.gitlab-ci.yml -license_scanning: - stage: coverage - image: - name: "registry.gitlab.com/gitlab-org/security-products/analyzers/ort/ort:latest" - script: - - /opt/ort/bin/ort analyze -i $CI_PROJECT_DIR -o $CI_PROJECT_DIR/ --allow-dynamic-versions - - /opt/ort/bin/ort scan -i $CI_PROJECT_DIR/analyzer-result.yml -o $CI_PROJECT_DIR/ || true - - /opt/ort/bin/ort report -f GitLabLicenseModel -i $CI_PROJECT_DIR/scan-result.yml -o $CI_PROJECT_DIR/ +#license_scanning: +# stage: coverage +# image: +# name: "registry.gitlab.com/gitlab-org/security-products/analyzers/ort/ort:latest" +# script: +# - /opt/ort/bin/ort analyze -i $CI_PROJECT_DIR -o $CI_PROJECT_DIR/ --allow-dynamic-versions +# - /opt/ort/bin/ort scan -i $CI_PROJECT_DIR/analyzer-result.yml -o $CI_PROJECT_DIR/ || true +# - /opt/ort/bin/ort report -f GitLabLicenseModel -i $CI_PROJECT_DIR/scan-result.yml -o $CI_PROJECT_DIR/ diff --git a/README.md b/README.md index f9ef355..e32d97d 100644 --- a/README.md +++ b/README.md @@ -26,45 +26,38 @@ import 'package:famedlysdk/famedlysdk.dart'; 2. Create a new client: ```dart -Client matrix = Client("HappyChat"); +Client client = Client("HappyChat"); ``` Take a look here for an example store: [https://gitlab.com/ChristianPauly/fluffychat-flutter/snippets](https://gitlab.com/ChristianPauly/fluffychat-flutter/snippets) -```dart -Client matrix = Client("HappyChat"); -``` - 3. Connect to a Matrix Homeserver and listen to the streams: ```dart -matrix.onLoginStateChanged.stream.listen((bool loginState){ +client.onLoginStateChanged.stream.listen((bool loginState){ print("LoginState: ${loginState.toString()}"); }); -matrix.onEvent.stream.listen((EventUpdate eventUpdate){ +client.onEvent.stream.listen((EventUpdate eventUpdate){ print("New event update!"); }); -matrix.onRoomUpdate.stream.listen((RoomUpdate eventUpdate){ +client.onRoomUpdate.stream.listen((RoomUpdate eventUpdate){ print("New room update!"); }); -final bool serverValid = await matrix.checkServer("https://yourhomeserver.abc"); - -final bool loginValid = await matrix.login("username", "password"); +try { + await client.checkHomeserver("https://yourhomeserver.abc"); + await client.login("username", "password"); +} +catch(e) { + print('No luck...'); +} ``` 4. Send a message to a Room: ```dart -final resp = await matrix.jsonRequest( - type: "PUT", - action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId", - data: { - "msgtype": "m.text", - "body": "hello" - } -); +await client.getRoomById('your_room_id').sendTextEvent('Hello world'); ``` diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index d487db5..63f4c6a 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -105,7 +105,7 @@ class Encryption { } Future handleEventUpdate(EventUpdate update) async { - if (update.type == 'ephemeral') { + if (update.type == EventUpdateType.ephemeral) { return; } if (update.eventType.startsWith('m.key.verification.') || @@ -159,17 +159,7 @@ class Encryption { var haveIndex = inboundGroupSession.indexes.containsKey(messageIndexKey); if (haveIndex && inboundGroupSession.indexes[messageIndexKey] != messageIndexValue) { - // TODO: maybe clear outbound session, if it is ours - // TODO: Make it so that we can't re-request the session keys, this is just for debugging Logs.error('[Decrypt] Could not decrypt due to a corrupted session.'); - Logs.error('[Decrypt] Want session: $roomId $sessionId $senderKey'); - Logs.error( - '[Decrypt] Have sessoin: ${inboundGroupSession.roomId} ${inboundGroupSession.sessionId} ${inboundGroupSession.senderKey}'); - Logs.error( - '[Decrypt] Want indexes: $messageIndexKey $messageIndexValue'); - Logs.error( - '[Decrypt] Have indexes: $messageIndexKey ${inboundGroupSession.indexes[messageIndexKey]}'); - canRequestSession = true; throw (DecryptError.CHANNEL_CORRUPTED); } inboundGroupSession.indexes[messageIndexKey] = messageIndexValue; @@ -188,13 +178,15 @@ class Encryption { } catch (exception) { // alright, if this was actually by our own outbound group session, we might as well clear it if (client.enableE2eeRecovery && + exception != DecryptError.UNKNOWN_SESSION && (keyManager .getOutboundGroupSession(roomId) ?.outboundGroupSession ?.session_id() ?? '') == event.content['session_id']) { - keyManager.clearOutboundGroupSession(roomId, wipe: true); + runInRoot(() => + keyManager.clearOrUseOutboundGroupSession(roomId, wipe: true)); } if (canRequestSession) { decryptedPayload = { @@ -235,8 +227,23 @@ class Encryption { } Future decryptRoomEvent(String roomId, Event event, - {bool store = false, String updateType = 'timeline'}) async { - final doStore = () async { + {bool store = false, + EventUpdateType updateType = EventUpdateType.timeline}) async { + if (event.type != EventTypes.Encrypted) { + return event; + } + if (client.database != null && + keyManager.getInboundGroupSession(roomId, event.content['session_id'], + event.content['sender_key']) == + null) { + await keyManager.loadInboundGroupSession( + roomId, event.content['session_id'], event.content['sender_key']); + } + event = decryptRoomEventSync(roomId, event); + if (event.type != EventTypes.Encrypted && store) { + if (updateType != EventUpdateType.history) { + event.room?.setState(event); + } await client.database?.storeEventUpdate( client.id, EventUpdate( @@ -247,28 +254,6 @@ class Encryption { sortOrder: event.sortOrder, ), ); - if (updateType != 'history') { - event.room?.setState(event); - } - }; - if (event.type != EventTypes.Encrypted) { - return event; - } - event = decryptRoomEventSync(roomId, event); - if (event.type != EventTypes.Encrypted) { - if (store) { - await doStore(); - } - return event; - } - if (client.database == null) { - return event; - } - await keyManager.loadInboundGroupSession( - roomId, event.content['session_id'], event.content['sender_key']); - event = decryptRoomEventSync(roomId, event); - if (event.type != EventTypes.Encrypted && store) { - await doStore(); } return event; } @@ -288,7 +273,7 @@ class Encryption { if (keyManager.getOutboundGroupSession(roomId) == null) { await keyManager.loadOutboundGroupSession(roomId); } - await keyManager.clearOutboundGroupSession(roomId); + await keyManager.clearOrUseOutboundGroupSession(roomId); if (keyManager.getOutboundGroupSession(roomId) == null) { await keyManager.createOutboundGroupSession(roomId); } @@ -314,7 +299,6 @@ class Encryption { 'session_id': sess.outboundGroupSession.session_id(), if (mRelatesTo != null) 'm.relates_to': mRelatesTo, }; - sess.sentMessages++; await keyManager.storeOutboundGroupSession(roomId, sess); return encryptedPayload; } diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 92e8a65..2b4da5d 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -150,9 +150,14 @@ class KeyManager { .markInboundGroupSessionAsUploaded(client.id, roomId, sessionId); } }); - // TODO: somehow try to decrypt last message again final room = client.getRoomById(roomId); if (room != null) { + // attempt to decrypt the last event + final event = room.getState(EventTypes.Encrypted); + if (event != null && event.content['session_id'] == sessionId) { + encryption.decryptRoomEvent(roomId, event, store: true); + } + // and finally broadcast the new session room.onSessionKeyReceived.add(sessionId); } } @@ -210,7 +215,7 @@ class KeyManager { // do e2ee recovery _requestedSessionIds.add(requestIdent); unawaited(runInRoot(() => - request(room, sessionId, senderKey, askOnlyOwnDevices: true))); + request(room, sessionId, senderKey, onlineKeyBackupOnly: true))); } return null; } @@ -226,6 +231,18 @@ class KeyManager { return sess; } + Map> _getDeviceKeyIdMap( + List deviceKeys) { + final deviceKeyIds = >{}; + for (final device in deviceKeys) { + if (!deviceKeyIds.containsKey(device.userId)) { + deviceKeyIds[device.userId] = {}; + } + deviceKeyIds[device.userId][device.deviceId] = device.blocked; + } + return deviceKeyIds; + } + /// clear all cached inbound group sessions. useful for testing void clearOutboundGroupSessions() { _outboundGroupSessions.clear(); @@ -233,8 +250,8 @@ class KeyManager { /// Clears the existing outboundGroupSession but first checks if the participating /// devices have been changed. Returns false if the session has not been cleared because - /// it wasn't necessary. - Future clearOutboundGroupSession(String roomId, + /// it wasn't necessary. Otherwise returns true. + Future clearOrUseOutboundGroupSession(String roomId, {bool wipe = false}) async { final room = client.getRoomById(roomId); final sess = getOutboundGroupSession(roomId); @@ -242,15 +259,7 @@ class KeyManager { return true; } if (!wipe) { - // first check if the devices in the room changed - final deviceKeys = await room.getUserDeviceKeys(); - deviceKeys.removeWhere((k) => k.blocked); - final deviceKeyIds = deviceKeys.map((k) => k.deviceId).toList(); - deviceKeyIds.sort(); - if (deviceKeyIds.toString() != sess.devices.toString()) { - wipe = true; - } - // next check if it needs to be rotated + // first check if it needs to be rotated final encryptionContent = room.getState(EventTypes.Encryption)?.content; final maxMessages = encryptionContent != null && encryptionContent['rotation_period_msgs'] is int @@ -266,7 +275,76 @@ class KeyManager { .isBefore(DateTime.now())) { wipe = true; } + } + if (!wipe) { + // next check if the devices in the room changed + final devicesToReceive = []; + final newDeviceKeys = await room.getUserDeviceKeys(); + final newDeviceKeyIds = _getDeviceKeyIdMap(newDeviceKeys); + // first check for user differences + final oldUserIds = Set.from(sess.devices.keys); + final newUserIds = Set.from(newDeviceKeyIds.keys); + if (oldUserIds.difference(newUserIds).isNotEmpty) { + // a user left the room, we must wipe the session + wipe = true; + } else { + final newUsers = newUserIds.difference(oldUserIds); + if (newUsers.isNotEmpty) { + // new user! Gotta send the megolm session to them + devicesToReceive + .addAll(newDeviceKeys.where((d) => newUsers.contains(d.userId))); + } + // okay, now we must test all the individual user devices, if anything new got blocked + // or if we need to send to any new devices. + // for this it is enough if we iterate over the old user Ids, as the new ones already have the needed keys in the list. + // we also know that all the old user IDs appear in the old one, else we have already wiped the session + for (final userId in oldUserIds) { + final oldBlockedDevices = Set.from(sess.devices[userId].entries + .where((e) => e.value) + .map((e) => e.key)); + final newBlockedDevices = Set.from(newDeviceKeyIds[userId] + .entries + .where((e) => e.value) + .map((e) => e.key)); + // we don't really care about old devices that got dropped (deleted), we only care if new ones got added and if new ones got blocked + // check if new devices got blocked + if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) { + wipe = true; + break; + } + // and now add all the new devices! + final oldDeviceIds = Set.from(sess.devices[userId].keys); + final newDeviceIds = Set.from(newDeviceKeyIds[userId].keys); + final newDevices = newDeviceIds.difference(oldDeviceIds); + if (newDeviceIds.isNotEmpty) { + devicesToReceive.addAll(newDeviceKeys.where( + (d) => d.userId == userId && newDevices.contains(d.deviceId))); + } + } + } + if (!wipe) { + // okay, we use the outbound group session! + sess.sentMessages++; + sess.devices = newDeviceKeyIds; + final rawSession = { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': room.id, + 'session_id': sess.outboundGroupSession.session_id(), + 'session_key': sess.outboundGroupSession.session_key(), + }; + try { + devicesToReceive.removeWhere((k) => k.blocked); + if (devicesToReceive.isNotEmpty) { + await client.sendToDeviceEncrypted( + devicesToReceive, 'm.room_key', rawSession); + } + } catch (e, s) { + Logs.error( + '[LibOlm] Unable to re-send the session key at later index to new devices: ' + + e.toString(), + s); + } return false; } } @@ -291,15 +369,13 @@ class KeyManager { } Future createOutboundGroupSession(String roomId) async { - await clearOutboundGroupSession(roomId, wipe: true); + await clearOrUseOutboundGroupSession(roomId, wipe: true); final room = client.getRoomById(roomId); if (room == null) { return null; } final deviceKeys = await room.getUserDeviceKeys(); - deviceKeys.removeWhere((k) => k.blocked); - final deviceKeyIds = deviceKeys.map((k) => k.deviceId).toList(); - deviceKeyIds.sort(); + final deviceKeyIds = _getDeviceKeyIdMap(deviceKeys); final outboundGroupSession = olm.OutboundGroupSession(); try { outboundGroupSession.create(); @@ -326,6 +402,7 @@ class KeyManager { key: client.userID, ); try { + deviceKeys.removeWhere((k) => k.blocked); await client.sendToDeviceEncrypted(deviceKeys, 'm.room_key', rawSession); await storeOutboundGroupSession(roomId, sess); _outboundGroupSessions[roomId] = sess; @@ -467,7 +544,7 @@ class KeyManager { String sessionId, String senderKey, { bool tryOnlineBackup = true, - bool askOnlyOwnDevices = false, + bool onlineKeyBackupOnly = false, }) async { if (tryOnlineBackup && await isCached()) { // let's first check our online key backup store thingy... @@ -491,13 +568,13 @@ class KeyManager { return; // we managed to load the session from online backup, no need to care about it now } } + if (onlineKeyBackupOnly) { + return; // we only want to do the online key backup + } try { // while we just send the to-device event to '*', we still need to save the // devices themself to know where to send the cancel to after receiving a reply final devices = await room.getUserDeviceKeys(); - if (askOnlyOwnDevices) { - devices.removeWhere((d) => d.userId != client.userID); - } final requestId = client.generateUniqueTransactionId(); final request = KeyManagerKeyShareRequest( requestId: requestId, diff --git a/lib/encryption/utils/outbound_group_session.dart b/lib/encryption/utils/outbound_group_session.dart index e9e2414..5de11cd 100644 --- a/lib/encryption/utils/outbound_group_session.dart +++ b/lib/encryption/utils/outbound_group_session.dart @@ -24,7 +24,11 @@ import '../../src/database/database.dart' show DbOutboundGroupSession; import '../../src/utils/logs.dart'; class OutboundGroupSession { - List devices; + /// The devices is a map from user id to device id to if the device is blocked. + /// This way we can easily know if a new user is added, leaves, a new devices is added, and, + /// very importantly, if we block a device. These are all important for determining if/when + /// an outbound session needs to be rotated. + Map> devices; DateTime creationTime; olm.OutboundGroupSession outboundGroupSession; int sentMessages; @@ -40,10 +44,21 @@ class OutboundGroupSession { OutboundGroupSession.fromDb(DbOutboundGroupSession dbEntry, String key) : key = key { + try { + devices = {}; + for (final entry in json.decode(dbEntry.deviceIds).entries) { + devices[entry.key] = Map.from(entry.value); + } + } catch (e) { + // devices is bad (old data), so just not use this session + Logs.info( + '[OutboundGroupSession] Session in database is old, not using it. ' + + e.toString()); + return; + } outboundGroupSession = olm.OutboundGroupSession(); try { outboundGroupSession.unpickle(key, dbEntry.pickle); - devices = List.from(json.decode(dbEntry.deviceIds)); creationTime = DateTime.fromMillisecondsSinceEpoch(dbEntry.creationTime); sentMessages = dbEntry.sentMessages; } catch (e, s) { diff --git a/lib/src/client.dart b/lib/src/client.dart index d5c9c15..7058364 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -256,43 +256,51 @@ class Client extends MatrixApi { return wellKnown; } - /// Checks the supported versions of the Matrix protocol and the supported - /// login types. Returns false if the server is not compatible with the - /// client. - /// Throws FormatException, TimeoutException and MatrixException on error. + @Deprecated('Use [checkHomeserver] instead.') Future checkServer(dynamic serverUrl) async { try { - if (serverUrl is Uri) { - homeserver = serverUrl; + await checkHomeserver(serverUrl); + } catch (_) { + return false; + } + return true; + } + + /// Checks the supported versions of the Matrix protocol and the supported + /// login types. Throws an exception if the server is not compatible with the + /// client and sets [homeserver] to [serverUrl] if it is. Supports the types [Uri] + /// and [String]. + Future checkHomeserver(dynamic homeserverUrl, + {Set supportedLoginTypes = supportedLoginTypes}) async { + try { + if (homeserverUrl is Uri) { + homeserver = homeserverUrl; } else { // URLs allow to have whitespace surrounding them, see https://www.w3.org/TR/2011/WD-html5-20110525/urls.html // As we want to strip a trailing slash, though, we have to trim the url ourself // and thus can't let Uri.parse() deal with it. - serverUrl = serverUrl.trim(); + homeserverUrl = homeserverUrl.trim(); // strip a trailing slash - if (serverUrl.endsWith('/')) { - serverUrl = serverUrl.substring(0, serverUrl.length - 1); + if (homeserverUrl.endsWith('/')) { + homeserverUrl = homeserverUrl.substring(0, homeserverUrl.length - 1); } - homeserver = Uri.parse(serverUrl); + homeserver = Uri.parse(homeserverUrl); } final versions = await requestSupportedVersions(); - for (var i = 0; i < versions.versions.length; i++) { - if (versions.versions[i] == 'r0.5.0' || - versions.versions[i] == 'r0.6.0') { - break; - } else if (i == versions.versions.length - 1) { - return false; - } + if (!versions.versions + .any((version) => supportedVersions.contains(version))) { + throw Exception( + 'Server supports the versions: ${versions.versions.toString()} but this application is only compatible with ${supportedVersions.toString()}.'); } final loginTypes = await requestLoginTypes(); - if (loginTypes.flows.indexWhere((f) => f.type == 'm.login.password') == - -1) { - return false; + if (!loginTypes.flows.any((f) => supportedLoginTypes.contains(f.type))) { + throw Exception( + 'Server supports the Login Types: ${loginTypes.flows.map((f) => f.toJson).toList().toString()} but this application is only compatible with ${supportedLoginTypes.toString()}.'); } - return true; + return; } catch (_) { homeserver = null; rethrow; @@ -301,7 +309,7 @@ class Client extends MatrixApi { /// Checks to see if a username is available, and valid, for the server. /// Returns the fully-qualified Matrix user ID (MXID) that has been registered. - /// You have to call [checkServer] first to set a homeserver. + /// You have to call [checkHomeserver] first to set a homeserver. @override Future register({ String username, @@ -339,7 +347,7 @@ class Client extends MatrixApi { /// Handles the login and allows the client to call all APIs which require /// authentication. Returns false if the login was not successful. Throws /// MatrixException if login was not successful. - /// You have to call [checkServer] first to set a homeserver. + /// You have to call [checkHomeserver] first to set a homeserver. @override Future login({ String type = 'm.login.password', @@ -503,8 +511,11 @@ class Client extends MatrixApi { ? PushRuleSet.fromJson(accountData['m.push_rules'].content) : null; - static String syncFilters = '{"room":{"state":{"lazy_load_members":true}}}'; - static String messagesFilters = '{"lazy_load_members":true}'; + static const Set supportedVersions = {'r0.5.0', 'r0.6.0'}; + static const Set supportedLoginTypes = {'m.login.password'}; + static const String syncFilters = + '{"room":{"state":{"lazy_load_members":true}}}'; + static const String messagesFilters = '{"lazy_load_members":true}'; static const List supportedDirectEncryptionAlgorithms = [ 'm.olm.v1.curve25519-aes-sha2' ]; @@ -911,14 +922,16 @@ class Client extends MatrixApi { if (room.state?.isNotEmpty ?? false) { // TODO: This method seems to be comperatively slow for some updates await _handleRoomEvents( - id, room.state.map((i) => i.toJson()).toList(), 'state'); + id, + room.state.map((i) => i.toJson()).toList(), + EventUpdateType.state); handledEvents = true; } if (room.timeline?.events?.isNotEmpty ?? false) { await _handleRoomEvents( id, room.timeline.events.map((i) => i.toJson()).toList(), - sortAtTheEnd ? 'history' : 'timeline', + sortAtTheEnd ? EventUpdateType.history : EventUpdateType.timeline, sortAtTheEnd: sortAtTheEnd); handledEvents = true; } @@ -928,30 +941,40 @@ class Client extends MatrixApi { id, room.ephemeral.map((i) => i.toJson()).toList()); } if (room.accountData?.isNotEmpty ?? false) { - await _handleRoomEvents(id, - room.accountData.map((i) => i.toJson()).toList(), 'account_data'); + await _handleRoomEvents( + id, + room.accountData.map((i) => i.toJson()).toList(), + EventUpdateType.accountData); } } if (room is LeftRoomUpdate) { if (room.timeline?.events?.isNotEmpty ?? false) { - await _handleRoomEvents(id, - room.timeline.events.map((i) => i.toJson()).toList(), 'timeline'); + await _handleRoomEvents( + id, + room.timeline.events.map((i) => i.toJson()).toList(), + EventUpdateType.timeline); handledEvents = true; } if (room.accountData?.isNotEmpty ?? false) { - await _handleRoomEvents(id, - room.accountData.map((i) => i.toJson()).toList(), 'account_data'); + await _handleRoomEvents( + id, + room.accountData.map((i) => i.toJson()).toList(), + EventUpdateType.accountData); } if (room.state?.isNotEmpty ?? false) { await _handleRoomEvents( - id, room.state.map((i) => i.toJson()).toList(), 'state'); + id, + room.state.map((i) => i.toJson()).toList(), + EventUpdateType.state); handledEvents = true; } } if (room is InvitedRoomUpdate && (room.inviteState?.isNotEmpty ?? false)) { - await _handleRoomEvents(id, - room.inviteState.map((i) => i.toJson()).toList(), 'invite_state'); + await _handleRoomEvents( + id, + room.inviteState.map((i) => i.toJson()).toList(), + EventUpdateType.inviteState); } if (handledEvents && database != null && roomObj != null) { await roomObj.updateSortOrder(); @@ -961,7 +984,7 @@ class Client extends MatrixApi { Future _handleEphemerals(String id, List events) async { for (num i = 0; i < events.length; i++) { - await _handleEvent(events[i], id, 'ephemeral'); + await _handleEvent(events[i], id, EventUpdateType.ephemeral); // Receipt events are deltas between two states. We will create a // fake room account data event for this and store the difference @@ -998,13 +1021,13 @@ class Client extends MatrixApi { } } events[i]['content'] = receiptStateContent; - await _handleEvent(events[i], id, 'account_data'); + await _handleEvent(events[i], id, EventUpdateType.accountData); } } } Future _handleRoomEvents( - String chat_id, List events, String type, + String chat_id, List events, EventUpdateType type, {bool sortAtTheEnd = false}) async { for (num i = 0; i < events.length; i++) { await _handleEvent(events[i], chat_id, type, sortAtTheEnd: sortAtTheEnd); @@ -1012,7 +1035,7 @@ class Client extends MatrixApi { } Future _handleEvent( - Map event, String roomID, String type, + Map event, String roomID, EventUpdateType type, {bool sortAtTheEnd = false}) async { if (event['type'] is String && event['content'] is Map) { // The client must ignore any new m.room.encryption event to prevent @@ -1028,7 +1051,7 @@ class Client extends MatrixApi { // 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' + final sortOrder = type != EventUpdateType.ephemeral ? (sortAtTheEnd ? room.oldSortOrder : room.newSortOrder) : 0.0; var update = EventUpdate( @@ -1051,7 +1074,7 @@ class Client extends MatrixApi { room.setState(user); } } - if (type != 'ephemeral' && database != null) { + if (type != EventUpdateType.ephemeral && database != null) { await database.storeEventUpdate(id, update); } _updateRoomsByEventUpdate(update); @@ -1062,7 +1085,7 @@ class Client extends MatrixApi { final rawUnencryptedEvent = update.content; - if (prevBatch != null && type == 'timeline') { + if (prevBatch != null && type == EventUpdateType.timeline) { if (rawUnencryptedEvent['type'] == EventTypes.CallInvite) { onCallInvite .add(Event.fromJson(rawUnencryptedEvent, room, sortOrder)); @@ -1141,15 +1164,15 @@ class Client extends MatrixApi { } void _updateRoomsByEventUpdate(EventUpdate eventUpdate) { - if (eventUpdate.type == 'history') return; + if (eventUpdate.type == EventUpdateType.history) return; final room = getRoomById(eventUpdate.roomID); if (room == null) return; switch (eventUpdate.type) { - case 'timeline': - case 'state': - case 'invite_state': + case EventUpdateType.timeline: + case EventUpdateType.state: + case EventUpdateType.inviteState: var stateEvent = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder); var prevState = room.getState(stateEvent.type, stateEvent.stateKey); @@ -1175,14 +1198,16 @@ sort order of ${prevState.sortOrder}. This should never happen...'''); room.setState(stateEvent); } break; - case 'account_data': + case EventUpdateType.accountData: room.roomAccountData[eventUpdate.eventType] = BasicRoomEvent.fromJson(eventUpdate.content); break; - case 'ephemeral': + case EventUpdateType.ephemeral: room.ephemerals[eventUpdate.eventType] = BasicRoomEvent.fromJson(eventUpdate.content); break; + case EventUpdateType.history: + break; } room.onUpdate.add(room.id); } diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 9de2ff3..e524305 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -6,7 +6,6 @@ import 'package:olm/olm.dart' as olm; import '../../famedlysdk.dart' as sdk; import '../../matrix_api.dart' as api; -import '../../matrix_api.dart'; import '../client.dart'; import '../room.dart'; import '../utils/logs.dart'; @@ -266,13 +265,13 @@ class Database extends _$Database { // let's see if we need any m.room.member events final membersToPostload = {}; // the lastEvent message preview might have an author we need to fetch, if it is a group chat - if (room.getState(EventTypes.Message) != null && !room.isDirectChat) { - membersToPostload.add(room.getState(EventTypes.Message).senderId); + if (room.getState(api.EventTypes.Message) != null && !room.isDirectChat) { + membersToPostload.add(room.getState(api.EventTypes.Message).senderId); } // if the room has no name and no canonical alias, its name is calculated // based on the heroes of the room - if (room.getState(EventTypes.RoomName) == null && - room.getState(EventTypes.RoomCanonicalAlias) == null && + if (room.getState(api.EventTypes.RoomName) == null && + room.getState(api.EventTypes.RoomCanonicalAlias) == null && room.mHeroes != null) { // we don't have a name and no canonical alias, so we'll need to // post-load the heroes @@ -430,7 +429,7 @@ class Database extends _$Database { /// [transaction]. Future storeEventUpdate( int clientId, sdk.EventUpdate eventUpdate) async { - if (eventUpdate.type == 'ephemeral') return; + if (eventUpdate.type == sdk.EventUpdateType.ephemeral) return; final eventContent = eventUpdate.content; final type = eventUpdate.type; final chatId = eventUpdate.roomID; @@ -441,11 +440,12 @@ class Database extends _$Database { stateKey = eventContent['state_key']; } - if (eventUpdate.eventType == EventTypes.Redaction) { + if (eventUpdate.eventType == api.EventTypes.Redaction) { await redactMessage(clientId, eventUpdate); } - if (type == 'timeline' || type == 'history') { + if (type == sdk.EventUpdateType.timeline || + type == sdk.EventUpdateType.history) { // calculate the status var status = 2; if (eventContent['unsigned'] is Map && @@ -493,7 +493,7 @@ class Database extends _$Database { } if (storeNewEvent) { DbEvent oldEvent; - if (type == 'history') { + if (type == sdk.EventUpdateType.history) { final allOldEvents = await getEvent(clientId, eventContent['event_id'], chatId).get(); if (allOldEvents.isNotEmpty) { @@ -527,12 +527,15 @@ class Database extends _$Database { } } - if (type == 'history') return; + if (type == sdk.EventUpdateType.history) return; - if (type != 'account_data' && + if (type != sdk.EventUpdateType.accountData && ((stateKey is String) || - [EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted] - .contains(eventUpdate.eventType))) { + [ + api.EventTypes.Message, + api.EventTypes.Sticker, + api.EventTypes.Encrypted + ].contains(eventUpdate.eventType))) { final now = DateTime.now(); await storeRoomState( clientId, @@ -547,7 +550,7 @@ class Database extends _$Database { json.encode(eventContent['prev_content'] ?? ''), stateKey ?? '', ); - } else if (type == 'account_data') { + } else if (type == sdk.EventUpdateType.accountData) { await storeRoomAccountData( clientId, eventContent['type'], diff --git a/lib/src/event.dart b/lib/src/event.dart index 6ec4df4..29202bd 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -316,7 +316,7 @@ class Event extends MatrixEvent { room.client.onEvent.add(EventUpdate( roomID: room.id, - type: 'timeline', + type: EventUpdateType.timeline, eventType: type, content: { 'event_id': eventId, diff --git a/lib/src/room.dart b/lib/src/room.dart index 80f34ba..2788d44 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -272,7 +272,15 @@ class Room { var lastEvent = lastEvents.isEmpty ? null - : lastEvents.reduce((a, b) => a.sortOrder > b.sortOrder ? a : b); + : lastEvents.reduce((a, b) { + if (a.sortOrder == b.sortOrder) { + // if two events have the same sort order we want to give encrypted events a lower priority + // This is so that if the same event exists in the state both encrypted *and* unencrypted, + // the unencrypted one is picked + return a.type == EventTypes.Encrypted ? b : a; + } + return a.sortOrder > b.sortOrder ? a : b; + }); if (lastEvent == null) { states.forEach((final String key, final entry) { if (!entry.containsKey('')) return; @@ -1190,7 +1198,7 @@ class Room { EventUpdate( content: content, roomID: id, - type: 'state', + type: EventUpdateType.state, eventType: EventTypes.RoomMember, sortOrder: 0.0), ); diff --git a/lib/src/timeline.dart b/lib/src/timeline.dart index 1ec96aa..6050423 100644 --- a/lib/src/timeline.dart +++ b/lib/src/timeline.dart @@ -118,7 +118,6 @@ class Timeline { for (var i = 0; i < events.length; i++) { if (events[i].type == EventTypes.Encrypted && events[i].messageType == MessageTypes.BadEncrypted && - events[i].content['can_request_session'] == true && events[i].content['session_id'] == sessionId) { events[i] = await room.client.encryption .decryptRoomEvent(room.id, events[i], store: true); @@ -211,7 +210,8 @@ class Timeline { try { if (eventUpdate.roomID != room.id) return; - if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') { + if (eventUpdate.type == EventUpdateType.timeline || + eventUpdate.type == EventUpdateType.history) { var status = eventUpdate.content['status'] ?? (eventUpdate.content['unsigned'] is Map ? eventUpdate.content['unsigned'][MessageSendingStatusKey] @@ -252,7 +252,7 @@ class Timeline { var newEvent = Event.fromJson( eventUpdate.content, room, eventUpdate.sortOrder); - if (eventUpdate.type == 'history' && + if (eventUpdate.type == EventUpdateType.history && events.indexWhere( (e) => e.eventId == eventUpdate.content['event_id']) != -1) return; diff --git a/lib/src/utils/event_update.dart b/lib/src/utils/event_update.dart index 514966b..de538b2 100644 --- a/lib/src/utils/event_update.dart +++ b/lib/src/utils/event_update.dart @@ -20,11 +20,20 @@ import '../../famedlysdk.dart'; import '../../matrix_api.dart'; import 'logs.dart'; +enum EventUpdateType { + timeline, + state, + history, + accountData, + ephemeral, + inviteState +} + /// Represents a new event (e.g. a message in a room) or an update for an /// already known event. class EventUpdate { /// Usually 'timeline', 'state' or whatever. - final String type; + final EventUpdateType type; /// Most events belong to a room. If not, this equals to eventType. final String roomID; diff --git a/lib/src/utils/markdown.dart b/lib/src/utils/markdown.dart index 81a882c..dea262c 100644 --- a/lib/src/utils/markdown.dart +++ b/lib/src/utils/markdown.dart @@ -76,12 +76,13 @@ class EmoteSyntax extends InlineSyntax { } } -class InlineLatexSyntax extends InlineSyntax { - InlineLatexSyntax() : super(r'(?<=\s|^)\$(?=\S)([^\n$]+)(?<=\S)\$(?=\s|$)'); +class InlineLatexSyntax extends TagSyntax { + InlineLatexSyntax() : super(r'\$', requiresDelimiterRun: true); @override - bool onMatch(InlineParser parser, Match match) { - final latex = htmlEscape.convert(match[1]); + bool onMatchEnd(InlineParser parser, Match match, TagState state) { + final latex = + htmlEscape.convert(parser.source.substring(state.endPos, match.start)); final element = Element('span', [Element.text('code', latex)]); element.attributes['data-mx-maths'] = latex; parser.addNode(element); @@ -89,22 +90,28 @@ class InlineLatexSyntax extends InlineSyntax { } } +// We also want to allow single-lines of like "$$latex$$" class BlockLatexSyntax extends BlockSyntax { @override - RegExp get pattern => RegExp(r'^[ ]{0,3}\${2}\s*$'); + RegExp get pattern => RegExp(r'^[ ]{0,3}\$\$(.*)$'); + + final endPattern = RegExp(r'^(.*)\$\$\s*$'); @override List parseChildLines(BlockParser parser) { var childLines = []; - parser.advance(); + var first = true; while (!parser.isDone) { - if (!pattern.hasMatch(parser.current)) { + final match = endPattern.firstMatch(parser.current); + if (match == null || (first && match.group(1).trim().isEmpty)) { childLines.add(parser.current); parser.advance(); } else { + childLines.add(match.group(1)); parser.advance(); break; } + first = false; } return childLines; } @@ -112,7 +119,9 @@ class BlockLatexSyntax extends BlockSyntax { @override Node parse(BlockParser parser) { final childLines = parseChildLines(parser); - final latex = htmlEscape.convert(childLines.join('\n')); + // we use .substring(2) as childLines will *always* contain the first two '$$' + final latex = + htmlEscape.convert(childLines.join('\n').trim().substring(2).trim()); final element = Element('div', [ Element('pre', [Element.text('code', latex)]) ]); diff --git a/test/client_test.dart b/test/client_test.dart index f1a13f4..1f125c9 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -77,11 +77,11 @@ void main() { expect(matrix.homeserver, null); try { - await matrix.checkServer('https://fakeserver.wrongaddress'); + await matrix.checkHomeserver('https://fakeserver.wrongaddress'); } on FormatException catch (exception) { expect(exception != null, true); } - await matrix.checkServer('https://fakeserver.notexisting'); + await matrix.checkHomeserver('https://fakeserver.notexisting'); expect(matrix.homeserver.toString(), 'https://fakeserver.notexisting'); final available = await matrix.usernameAvailable('testuser'); @@ -240,55 +240,55 @@ void main() { expect(eventUpdateList[0].eventType, 'm.room.member'); expect(eventUpdateList[0].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[0].type, 'state'); + expect(eventUpdateList[0].type, EventUpdateType.state); expect(eventUpdateList[1].eventType, 'm.room.canonical_alias'); expect(eventUpdateList[1].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[1].type, 'state'); + expect(eventUpdateList[1].type, EventUpdateType.state); expect(eventUpdateList[2].eventType, 'm.room.encryption'); expect(eventUpdateList[2].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[2].type, 'state'); + expect(eventUpdateList[2].type, EventUpdateType.state); expect(eventUpdateList[3].eventType, 'm.room.pinned_events'); expect(eventUpdateList[3].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[3].type, 'state'); + expect(eventUpdateList[3].type, EventUpdateType.state); expect(eventUpdateList[4].eventType, 'm.room.member'); expect(eventUpdateList[4].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[4].type, 'timeline'); + expect(eventUpdateList[4].type, EventUpdateType.timeline); expect(eventUpdateList[5].eventType, 'm.room.message'); expect(eventUpdateList[5].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[5].type, 'timeline'); + expect(eventUpdateList[5].type, EventUpdateType.timeline); expect(eventUpdateList[6].eventType, 'm.typing'); expect(eventUpdateList[6].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[6].type, 'ephemeral'); + expect(eventUpdateList[6].type, EventUpdateType.ephemeral); expect(eventUpdateList[7].eventType, 'm.receipt'); expect(eventUpdateList[7].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[7].type, 'ephemeral'); + expect(eventUpdateList[7].type, EventUpdateType.ephemeral); expect(eventUpdateList[8].eventType, 'm.receipt'); expect(eventUpdateList[8].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[8].type, 'account_data'); + expect(eventUpdateList[8].type, EventUpdateType.accountData); expect(eventUpdateList[9].eventType, 'm.tag'); expect(eventUpdateList[9].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[9].type, 'account_data'); + expect(eventUpdateList[9].type, EventUpdateType.accountData); expect(eventUpdateList[10].eventType, 'org.example.custom.room.config'); expect(eventUpdateList[10].roomID, '!726s6s6q:example.com'); - expect(eventUpdateList[10].type, 'account_data'); + expect(eventUpdateList[10].type, EventUpdateType.accountData); expect(eventUpdateList[11].eventType, 'm.room.name'); expect(eventUpdateList[11].roomID, '!696r7674:example.com'); - expect(eventUpdateList[11].type, 'invite_state'); + expect(eventUpdateList[11].type, EventUpdateType.inviteState); expect(eventUpdateList[12].eventType, 'm.room.member'); expect(eventUpdateList[12].roomID, '!696r7674:example.com'); - expect(eventUpdateList[12].type, 'invite_state'); + expect(eventUpdateList[12].type, EventUpdateType.inviteState); }); test('To Device Update Test', () async { @@ -311,12 +311,11 @@ void main() { roomUpdateListFuture = matrix.onRoomUpdate.stream.toList(); eventUpdateListFuture = matrix.onEvent.stream.toList(); - final checkResp = - await matrix.checkServer('https://fakeServer.notExisting'); + + await matrix.checkHomeserver('https://fakeServer.notExisting'); final loginResp = await matrix.login(user: 'test', password: '1234'); - expect(checkResp, true); expect(loginResp != null, true); }); diff --git a/test/encryption/encrypt_decrypt_to_device_test.dart b/test/encryption/encrypt_decrypt_to_device_test.dart index 4fbde05..3a95f75 100644 --- a/test/encryption/encrypt_decrypt_to_device_test.dart +++ b/test/encryption/encrypt_decrypt_to_device_test.dart @@ -50,7 +50,7 @@ void main() { test('setupClient', () async { client = await getClient(); otherClient.database = client.database; - await otherClient.checkServer('https://fakeServer.notExisting'); + await otherClient.checkHomeserver('https://fakeServer.notExisting'); otherClient.connect( newToken: 'abc', newUserID: '@othertest:fakeServer.notExisting', diff --git a/test/encryption/key_manager_test.dart b/test/encryption/key_manager_test.dart index 1dea9c4..6d3783b 100644 --- a/test/encryption/key_manager_test.dart +++ b/test/encryption/key_manager_test.dart @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import 'dart:convert'; + import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; @@ -100,7 +102,7 @@ void main() { expect( client.encryption.keyManager.getOutboundGroupSession(roomId) != null, true); - await client.encryption.keyManager.clearOutboundGroupSession(roomId); + await client.encryption.keyManager.clearOrUseOutboundGroupSession(roomId); expect( client.encryption.keyManager.getOutboundGroupSession(roomId) != null, true); @@ -112,17 +114,17 @@ void main() { // rotate after too many messages sess.sentMessages = 300; - await client.encryption.keyManager.clearOutboundGroupSession(roomId); + await client.encryption.keyManager.clearOrUseOutboundGroupSession(roomId); expect( client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); - // rotate if devices in room change + // rotate if device is blocked sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'] .blocked = true; - await client.encryption.keyManager.clearOutboundGroupSession(roomId); + await client.encryption.keyManager.clearOrUseOutboundGroupSession(roomId); expect( client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); @@ -133,16 +135,61 @@ void main() { sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); sess.creationTime = DateTime.now().subtract(Duration(days: 30)); - await client.encryption.keyManager.clearOutboundGroupSession(roomId); + await client.encryption.keyManager.clearOrUseOutboundGroupSession(roomId); expect( client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + // rotate if user leaves + sess = + await client.encryption.keyManager.createOutboundGroupSession(roomId); + final room = client.getRoomById(roomId); + final member = room.getState('m.room.member', '@alice:example.com'); + member.content['membership'] = 'leave'; + room.mJoinedMemberCount--; + await client.encryption.keyManager.clearOrUseOutboundGroupSession(roomId); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + false); + member.content['membership'] = 'join'; + room.mJoinedMemberCount++; + + // do not rotate if new device is added + sess = + await client.encryption.keyManager.createOutboundGroupSession(roomId); + client.userDeviceKeys['@alice:example.com'].deviceKeys['NEWDEVICE'] = + DeviceKeys.fromJson({ + 'user_id': '@alice:example.com', + 'device_id': 'NEWDEVICE', + 'algorithms': ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], + 'keys': { + 'curve25519:JLAFKJWSCS': + '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI', + 'ed25519:JLAFKJWSCS': 'lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI' + }, + }, client); + await client.encryption.keyManager.clearOrUseOutboundGroupSession(roomId); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + true); + + // do not rotate if new user is added + member.content['membership'] = 'leave'; + room.mJoinedMemberCount--; + sess = + await client.encryption.keyManager.createOutboundGroupSession(roomId); + member.content['membership'] = 'join'; + room.mJoinedMemberCount++; + await client.encryption.keyManager.clearOrUseOutboundGroupSession(roomId); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + true); + // force wipe sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); await client.encryption.keyManager - .clearOutboundGroupSession(roomId, wipe: true); + .clearOrUseOutboundGroupSession(roomId, wipe: true); expect( client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); @@ -249,6 +296,30 @@ void main() { final senderKey = client.identityKey; final roomId = '!someroom:example.org'; final sessionId = inbound.session_id(); + final room = Room(id: roomId, client: client); + client.rooms.add(room); + // we build up an encrypted message so that we can test if it successfully decrypted afterwards + room.states['m.room.encrypted'] = Event( + senderId: '@test:example.com', + type: 'm.room.encrypted', + roomId: room.id, + room: room, + eventId: '12345', + originServerTs: DateTime.now(), + content: { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'ciphertext': session.encrypt(json.encode({ + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'foxies'}, + })), + 'device_id': client.deviceID, + 'sender_key': client.identityKey, + 'session_id': sessionId, + }, + stateKey: '', + sortOrder: 42.0, + ); + expect(room.lastEvent.type, 'm.room.encrypted'); // set a payload... var sessionPayload = { 'algorithm': 'm.megolm.v1.aes-sha2', @@ -379,6 +450,10 @@ void main() { .length, 0); + // test that it decrypted the last event + expect(room.lastEvent.type, 'm.room.message'); + expect(room.lastEvent.content['body'], 'foxies'); + inbound.free(); session.free(); }); diff --git a/test/encryption/key_verification_test.dart b/test/encryption/key_verification_test.dart index 8d7c86b..27db626 100644 --- a/test/encryption/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -54,7 +54,7 @@ EventUpdate getLastSentEvent(KeyVerification req) { 'sender': req.client.userID, }, eventType: type, - type: 'timeline', + type: EventUpdateType.timeline, roomID: req.room.id, ); } @@ -85,7 +85,7 @@ void main() { client1 = await getClient(); client2 = Client('othertestclient', httpClient: FakeMatrixApi()); client2.database = client1.database; - await client2.checkServer('https://fakeServer.notExisting'); + await client2.checkHomeserver('https://fakeServer.notExisting'); client2.connect( newToken: 'abc', newUserID: '@othertest:fakeServer.notExisting', @@ -446,7 +446,7 @@ void main() { 'sender': client2.userID, }, eventType: 'm.key.verification.ready', - type: 'timeline', + type: EventUpdateType.timeline, roomID: req2.room.id, )); expect(req2.state, KeyVerificationState.error); diff --git a/test/event_test.dart b/test/event_test.dart index 74d7c6a..645d7b0 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -264,7 +264,7 @@ void main() { test('sendAgain', () async { var matrix = Client('testclient', httpClient: FakeMatrixApi()); - await matrix.checkServer('https://fakeServer.notExisting'); + await matrix.checkHomeserver('https://fakeServer.notExisting'); await matrix.login(user: 'test', password: '1234'); var event = Event.fromJson( @@ -280,7 +280,7 @@ void main() { test('requestKey', () async { var matrix = Client('testclient', httpClient: FakeMatrixApi()); - await matrix.checkServer('https://fakeServer.notExisting'); + await matrix.checkHomeserver('https://fakeServer.notExisting'); await matrix.login(user: 'test', password: '1234'); var event = Event.fromJson( @@ -990,7 +990,7 @@ void main() { THUMBNAIL_BUFF, }[url]; }; - await client.checkServer('https://fakeServer.notExisting'); + await client.checkHomeserver('https://fakeServer.notExisting'); final room = Room(id: '!localpart:server.abc', client: client); var event = Event.fromJson({ 'type': EventTypes.Message, @@ -1133,7 +1133,7 @@ void main() { FILE_BUFF, }[url]; }; - await client.checkServer('https://fakeServer.notExisting'); + await client.checkHomeserver('https://fakeServer.notExisting'); final room = Room(id: '!localpart:server.abc', client: await getClient()); var event = Event.fromJson({ 'type': EventTypes.Message, diff --git a/test/fake_client.dart b/test/fake_client.dart index 5f30487..bcc7f18 100644 --- a/test/fake_client.dart +++ b/test/fake_client.dart @@ -31,7 +31,7 @@ const pickledOlmAccount = Future getClient() async { final client = Client('testclient', httpClient: FakeMatrixApi()); client.database = getDatabase(); - await client.checkServer('https://fakeServer.notExisting'); + await client.checkHomeserver('https://fakeServer.notExisting'); client.connect( newToken: 'abcd', newUserID: '@test:fakeServer.notExisting', diff --git a/test/markdown_test.dart b/test/markdown_test.dart index 0b172d6..b2aecff 100644 --- a/test/markdown_test.dart +++ b/test/markdown_test.dart @@ -73,6 +73,16 @@ void main() { test('latex', () { expect(markdown('meep \$\\frac{2}{3}\$'), 'meep \\frac{2}{3}'); + expect(markdown('meep \$hmm *yay*\$'), + 'meep hmm *yay*'); + expect(markdown('you have \$somevar and \$someothervar'), + 'you have \$somevar and \$someothervar'); + expect(markdown('meep ||\$\\frac{2}{3}\$||'), + 'meep \\frac{2}{3}'); + expect(markdown('meep `\$\\frac{2}{3}\$`'), + 'meep \$\\frac{2}{3}\$'); + expect(markdown('hey\n\$\$beep\$\$\nmeow'), + '

hey

\n
\n
beep
\n
\n

meow

'); expect(markdown('hey\n\$\$\nbeep\nboop\n\$\$\nmeow'), '

hey

\n
\n
beep\nboop
\n
\n

meow

'); }); diff --git a/test/matrix_database_test.dart b/test/matrix_database_test.dart index 0f16b63..f41eebe 100644 --- a/test/matrix_database_test.dart +++ b/test/matrix_database_test.dart @@ -40,7 +40,7 @@ void main() { test('storeEventUpdate', () async { // store a simple update var update = EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: room.id, eventType: 'm.room.message', content: { @@ -58,7 +58,7 @@ void main() { // insert a transaction id update = EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: room.id, eventType: 'm.room.message', content: { @@ -75,7 +75,7 @@ void main() { event = await database.getEventById(clientId, 'transaction-1', room); expect(event.eventId, 'transaction-1'); update = EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: room.id, eventType: 'm.room.message', content: { @@ -98,7 +98,7 @@ void main() { // insert a transaction id if the event id for it already exists update = EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: room.id, eventType: 'm.room.message', content: { @@ -115,7 +115,7 @@ void main() { event = await database.getEventById(clientId, '\$event-3', room); expect(event.eventId, '\$event-3'); update = EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: room.id, eventType: 'm.room.message', content: { @@ -140,7 +140,7 @@ void main() { // insert transaction id and not update status update = EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: room.id, eventType: 'm.room.message', content: { @@ -157,7 +157,7 @@ void main() { event = await database.getEventById(clientId, '\$event-4', room); expect(event.eventId, '\$event-4'); update = EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: room.id, eventType: 'm.room.message', content: { diff --git a/test/mxc_uri_extension_test.dart b/test/mxc_uri_extension_test.dart index 30e6f84..37fdca2 100644 --- a/test/mxc_uri_extension_test.dart +++ b/test/mxc_uri_extension_test.dart @@ -27,7 +27,7 @@ void main() { group('MxContent', () { test('Formatting', () async { var client = Client('testclient', httpClient: FakeMatrixApi()); - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); final mxc = 'mxc://exampleserver.abc/abcdefghijklmn'; final content = Uri.parse(mxc); expect(content.isScheme('mxc'), true); diff --git a/test/room_test.dart b/test/room_test.dart index 53cc2d7..38f96dc 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -179,6 +179,31 @@ void main() { expect(room.timeCreated, room.lastEvent.originServerTs); }); + test('multiple last event with same sort order', () { + room.states['m.room.encrypted'] = Event( + senderId: '@test:example.com', + type: 'm.room.encrypted', + roomId: room.id, + room: room, + eventId: '12345', + originServerTs: DateTime.now(), + content: {'msgtype': 'm.text', 'body': 'test'}, + stateKey: '', + sortOrder: 42.0); + expect(room.lastEvent.type, 'm.room.encrypted'); + room.states['m.room.messge'] = Event( + senderId: '@test:example.com', + type: 'm.room.messge', + roomId: room.id, + room: room, + eventId: '12345', + originServerTs: DateTime.now(), + content: {'msgtype': 'm.text', 'body': 'test'}, + stateKey: '', + sortOrder: 42.0); + expect(room.lastEvent.type, 'm.room.encrypted'); + }); + test('sendReadReceipt', () async { await room.sendReadReceipt('ยง1234:fakeServer.notExisting'); }); diff --git a/test/timeline_test.dart b/test/timeline_test.dart index 8717e75..428e772 100644 --- a/test/timeline_test.dart +++ b/test/timeline_test.dart @@ -49,10 +49,10 @@ void main() { }); test('Create', () async { - await client.checkServer('https://fakeServer.notExisting'); + await client.checkHomeserver('https://fakeServer.notExisting'); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -65,7 +65,7 @@ void main() { }, sortOrder: room.newSortOrder)); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -114,7 +114,7 @@ void main() { expect(timeline.events[0].receipts[0].user.id, '@alice:example.com'); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.redaction', content: { @@ -149,7 +149,7 @@ void main() { expect(timeline.events[0].status, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -174,7 +174,7 @@ void main() { test('Send message with error', () async { client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -221,7 +221,7 @@ void main() { test('Resend message', () async { timeline.events.clear(); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -278,7 +278,7 @@ void main() { test('sort errors on top', () async { timeline.events.clear(); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -291,7 +291,7 @@ void main() { }, sortOrder: room.newSortOrder)); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -311,7 +311,7 @@ void main() { test('sending event to failed update', () async { timeline.events.clear(); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -327,7 +327,7 @@ void main() { expect(timeline.events[0].status, 0); expect(timeline.events.length, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -347,7 +347,7 @@ void main() { () async { timeline.events.clear(); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -363,7 +363,7 @@ void main() { expect(timeline.events[0].status, 0); expect(timeline.events.length, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -380,7 +380,7 @@ void main() { expect(timeline.events[0].status, 1); expect(timeline.events.length, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -401,7 +401,7 @@ void main() { () async { timeline.events.clear(); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -420,7 +420,7 @@ void main() { expect(timeline.events[0].status, 0); expect(timeline.events.length, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -439,7 +439,7 @@ void main() { expect(timeline.events[0].status, 2); expect(timeline.events.length, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -461,7 +461,7 @@ void main() { test('sending an event 0 -> -1 -> 2', () async { timeline.events.clear(); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -477,7 +477,7 @@ void main() { expect(timeline.events[0].status, 0); expect(timeline.events.length, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -493,7 +493,7 @@ void main() { expect(timeline.events[0].status, -1); expect(timeline.events.length, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -513,7 +513,7 @@ void main() { test('sending an event 0 -> 2 -> -1', () async { timeline.events.clear(); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -529,7 +529,7 @@ void main() { expect(timeline.events[0].status, 0); expect(timeline.events.length, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { @@ -546,7 +546,7 @@ void main() { expect(timeline.events[0].status, 2); expect(timeline.events.length, 1); client.onEvent.add(EventUpdate( - type: 'timeline', + type: EventUpdateType.timeline, roomID: roomID, eventType: 'm.room.message', content: { diff --git a/test/user_test.dart b/test/user_test.dart index fdb8c0b..e136bb8 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -85,28 +85,28 @@ void main() { user3.calcDisplayname(mxidLocalPartFallback: false), 'Unknown user'); }); test('kick', () async { - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); await user1.kick(); }); test('ban', () async { - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); await user1.ban(); }); test('unban', () async { - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); await user1.unban(); }); test('setPower', () async { - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); await user1.setPower(50); }); test('startDirectChat', () async { - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); await client.login(user: 'test', password: '1234'); await user1.startDirectChat(); }); test('getPresence', () async { - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); await client.handleSync(SyncUpdate.fromJson({ 'presence': { 'events': [ @@ -121,15 +121,15 @@ void main() { expect(user1.presence.presence.presence, PresenceType.online); }); test('canBan', () async { - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); expect(user1.canBan, false); }); test('canKick', () async { - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); expect(user1.canKick, false); }); test('canChangePowerLevel', () async { - await client.checkServer('https://fakeserver.notexisting'); + await client.checkHomeserver('https://fakeserver.notexisting'); expect(user1.canChangePowerLevel, false); }); test('dispose client', () async { diff --git a/test_driver/famedlysdk_test.dart b/test_driver/famedlysdk_test.dart index 1c19612..81300e4 100644 --- a/test_driver/famedlysdk_test.dart +++ b/test_driver/famedlysdk_test.dart @@ -24,7 +24,7 @@ void test() async { Logs.success('++++ Login Alice at ++++'); testClientA = Client('TestClientA'); testClientA.database = getDatabase(); - await testClientA.checkServer(TestUser.homeserver); + await testClientA.checkHomeserver(TestUser.homeserver); await testClientA.login( user: TestUser.username, password: TestUser.password); assert(testClientA.encryptionEnabled); @@ -32,7 +32,7 @@ void test() async { Logs.success('++++ Login Bob ++++'); testClientB = Client('TestClientB'); testClientB.database = getDatabase(); - await testClientB.checkServer(TestUser.homeserver); + await testClientB.checkHomeserver(TestUser.homeserver); await testClientB.login( user: TestUser.username2, password: TestUser.password); assert(testClientB.encryptionEnabled); @@ -222,7 +222,7 @@ void test() async { Logs.success('++++ Login Bob in another client ++++'); var testClientC = Client('TestClientC', database: getDatabase()); - await testClientC.checkServer(TestUser.homeserver); + await testClientC.checkHomeserver(TestUser.homeserver); await testClientC.login( user: TestUser.username2, password: TestUser.password); await Future.delayed(Duration(seconds: 3));