diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 36458ff..33c4799 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -68,7 +68,7 @@ build-api-doc: paths: - doc/api/ only: - - master + - main build-doc: tags: @@ -83,7 +83,7 @@ build-doc: paths: - doc-public only: - - master + - main pages: tags: @@ -101,4 +101,4 @@ pages: paths: - public only: - - master \ No newline at end of file + - main \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index c0e32f0..a3efac9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,9 +3,11 @@ include: package:pedantic/analysis_options.yaml linter: rules: - camel_case_types + - avoid_print analyzer: errors: todo: ignore -# exclude: -# - path/to/excluded/files/** \ No newline at end of file + exclude: + - example/main.dart + - lib/src/utils/logs.dart \ No newline at end of file diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 0000000..b880ffc --- /dev/null +++ b/example/main.dart @@ -0,0 +1,264 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(FamedlySdkExampleApp()); +} + +class FamedlySdkExampleApp extends StatelessWidget { + static Client client = Client('Famedly SDK Example Client', debug: true); + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Famedly SDK Example App', + home: LoginView(), + ); + } +} + +class LoginView extends StatefulWidget { + @override + _LoginViewState createState() => _LoginViewState(); +} + +class _LoginViewState extends State { + final TextEditingController _homeserverController = TextEditingController(); + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + bool _isLoading = false; + String _error; + + void _loginAction() async { + setState(() => _isLoading = true); + setState(() => _error = null); + try { + if (await FamedlySdkExampleApp.client + .checkServer(_homeserverController.text) == + false) { + throw (Exception('Server not supported')); + } + if (await FamedlySdkExampleApp.client.login( + _usernameController.text, + _passwordController.text, + ) == + false) { + throw (Exception('Username or password incorrect')); + } + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => ChatListView()), + (route) => false, + ); + } catch (e) { + setState(() => _error = e.toString()); + } + setState(() => _isLoading = false); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Login')), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + TextField( + controller: _homeserverController, + readOnly: _isLoading, + autocorrect: false, + decoration: InputDecoration( + labelText: 'Homeserver', + hintText: 'https://matrix.org', + ), + ), + SizedBox(height: 8), + TextField( + controller: _usernameController, + readOnly: _isLoading, + autocorrect: false, + decoration: InputDecoration( + labelText: 'Username', + hintText: '@username:domain', + ), + ), + SizedBox(height: 8), + TextField( + controller: _passwordController, + obscureText: true, + readOnly: _isLoading, + autocorrect: false, + decoration: InputDecoration( + labelText: 'Password', + hintText: '****', + errorText: _error, + ), + ), + SizedBox(height: 8), + RaisedButton( + child: _isLoading ? LinearProgressIndicator() : Text('Login'), + onPressed: _isLoading ? null : _loginAction, + ), + ], + ), + ); + } +} + +class ChatListView extends StatefulWidget { + @override + _ChatListViewState createState() => _ChatListViewState(); +} + +class _ChatListViewState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Chats'), + ), + body: StreamBuilder( + stream: FamedlySdkExampleApp.client.onSync.stream, + builder: (c, s) => ListView.builder( + itemCount: FamedlySdkExampleApp.client.rooms.length, + itemBuilder: (BuildContext context, int i) { + final room = FamedlySdkExampleApp.client.rooms[i]; + return ListTile( + title: Text(room.displayname + ' (${room.notificationCount})'), + subtitle: Text(room.lastMessage, maxLines: 1), + leading: CircleAvatar( + backgroundImage: NetworkImage(room.avatar.getThumbnail( + FamedlySdkExampleApp.client, + width: 64, + height: 64, + )), + ), + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ChatView(room: room), + ), + ), + ); + }, + ), + ), + ); + } +} + +class ChatView extends StatefulWidget { + final Room room; + + const ChatView({Key key, @required this.room}) : super(key: key); + + @override + _ChatViewState createState() => _ChatViewState(); +} + +class _ChatViewState extends State { + final TextEditingController _controller = TextEditingController(); + + void _sendAction() { + print('Send Text'); + widget.room.sendTextEvent(_controller.text); + _controller.clear(); + } + + Timeline timeline; + + Future getTimeline() async { + timeline ??= + await widget.room.getTimeline(onUpdate: () => setState(() => null)); + return true; + } + + @override + void dispose() { + timeline?.cancelSubscriptions(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: StreamBuilder( + stream: widget.room.onUpdate.stream, + builder: (context, snapshot) { + return Text(widget.room.displayname); + }), + ), + body: Column( + children: [ + Expanded( + child: FutureBuilder( + future: getTimeline(), + builder: (context, snapshot) => !snapshot.hasData + ? Center( + child: CircularProgressIndicator(), + ) + : ListView.builder( + reverse: true, + itemCount: timeline.events.length, + itemBuilder: (BuildContext context, int i) => Opacity( + opacity: timeline.events[i].status != 2 ? 0.5 : 1, + child: ListTile( + title: Row( + children: [ + Expanded( + child: Text( + timeline.events[i].sender.calcDisplayname(), + ), + ), + Text( + timeline.events[i].originServerTs + .toIso8601String(), + style: TextStyle(fontSize: 12), + ), + ], + ), + subtitle: Text(timeline.events[i].body), + leading: CircleAvatar( + child: timeline.events[i].sender?.avatarUrl == null + ? Icon(Icons.person) + : null, + backgroundImage: + timeline.events[i].sender?.avatarUrl != null + ? NetworkImage( + timeline.events[i].sender?.avatarUrl + ?.getThumbnail( + FamedlySdkExampleApp.client, + width: 64, + height: 64, + ), + ) + : null, + ), + ), + ), + ), + ), + ), + Container( + height: 60, + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + labelText: 'Send a message ...', + ), + ), + ), + IconButton( + icon: Icon(Icons.send), + onPressed: _sendAction, + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart index 92cbb86..c8e2249 100644 --- a/lib/encryption/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -167,7 +167,7 @@ class CrossSigning { } if (signedKeys.isNotEmpty) { // post our new keys! - await client.api.uploadKeySignatures(signedKeys); + await client.uploadKeySignatures(signedKeys); } } diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index 940f4fa..62c5a05 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -72,12 +72,16 @@ class Encryption { } Future handleToDeviceEvent(ToDeviceEvent event) async { - if (['m.room_key', 'm.room_key_request', 'm.forwarded_room_key'] - .contains(event.type)) { - // a new room key or thelike. We need to handle this asap, before other + if (event.type == 'm.room_key') { + // a new room key. We need to handle this asap, before other // events in /sync are handled await keyManager.handleToDeviceEvent(event); } + if (['m.room_key_request', 'm.forwarded_room_key'].contains(event.type)) { + // "just" room key request things. We don't need these asap, so we handle + // them in the background + unawaited(keyManager.handleToDeviceEvent(event)); + } if (event.type.startsWith('m.key.verification.')) { // some key verification event. No need to handle it now, we can easily // do this in the background @@ -263,6 +267,9 @@ class Encryption { if (sess == null) { throw ('Unable to create new outbound group session'); } + // we clone the payload as we do not want to remove 'm.relates_to' from the + // original payload passed into this function + payload = Map.from(payload); final Map mRelatesTo = payload.remove('m.relates_to'); final payloadContent = { 'content': payload, diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index e60cacb..a0e1809 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -18,6 +18,7 @@ import 'dart:convert'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:pedantic/pedantic.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; @@ -43,7 +44,7 @@ class KeyManager { encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async { final keyObj = olm.PkDecryption(); try { - final info = await client.api.getRoomKeysBackup(); + final info = await client.getRoomKeysBackup(); if (info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2) { return false; } @@ -84,10 +85,11 @@ class KeyManager { } else { inboundGroupSession.create(content['session_key']); } - } catch (e) { + } catch (e, s) { inboundGroupSession.free(); - print( - '[LibOlm] Could not create new InboundGroupSession: ' + e.toString()); + Logs.error( + '[LibOlm] Could not create new InboundGroupSession: ' + e.toString(), + s); return; } final newSession = SessionKey( @@ -123,6 +125,8 @@ class KeyManager { json.encode(content), json.encode({}), ); + // Note to self: When adding key-backup that needs to be unawaited(), else + // we might accidentally end up with http requests inside of the sync loop // TODO: somehow try to decrypt last message again final room = client.getRoomById(roomId); if (room != null) { @@ -261,10 +265,11 @@ class KeyManager { final outboundGroupSession = olm.OutboundGroupSession(); try { outboundGroupSession.create(); - } catch (e) { + } catch (e, s) { outboundGroupSession.free(); - print('[LibOlm] Unable to create new outboundGroupSession: ' + - e.toString()); + Logs.error( + '[LibOlm] Unable to create new outboundGroupSession: ' + e.toString(), + s); return null; } final rawSession = { @@ -283,14 +288,14 @@ class KeyManager { key: client.userID, ); try { - await client.sendToDevice(deviceKeys, 'm.room_key', rawSession); + await client.sendToDeviceEncrypted(deviceKeys, 'm.room_key', rawSession); await storeOutboundGroupSession(roomId, sess); _outboundGroupSessions[roomId] = sess; } catch (e, s) { - print( + Logs.error( '[LibOlm] Unable to send the session key to the participating devices: ' + - e.toString()); - print(s); + e.toString(), + s); sess.dispose(); return null; } @@ -334,7 +339,7 @@ class KeyManager { final privateKey = base64.decode(await encryption.ssss.getCached(MEGOLM_KEY)); final decryption = olm.PkDecryption(); - final info = await client.api.getRoomKeysBackup(); + final info = await client.getRoomKeysBackup(); String backupPubKey; try { backupPubKey = decryption.init_with_private_key(privateKey); @@ -363,8 +368,9 @@ class KeyManager { try { decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], sessionData['mac'], sessionData['ciphertext'])); - } catch (err) { - print('[LibOlm] Error decrypting room key: ' + err.toString()); + } catch (e, s) { + Logs.error( + '[LibOlm] Error decrypting room key: ' + e.toString(), s); } if (decrypted != null) { decrypted['session_id'] = sessionId; @@ -381,9 +387,9 @@ class KeyManager { } Future loadSingleKey(String roomId, String sessionId) async { - final info = await client.api.getRoomKeysBackup(); + final info = await client.getRoomKeysBackup(); final ret = - await client.api.getRoomKeysSingleKey(roomId, sessionId, info.version); + await client.getRoomKeysSingleKey(roomId, sessionId, info.version); final keys = RoomKeys.fromJson({ 'rooms': { roomId: { @@ -406,28 +412,31 @@ class KeyManager { try { await loadSingleKey(room.id, sessionId); } catch (err, stacktrace) { - print('[KeyManager] Failed to access online key backup: ' + - err.toString()); - print(stacktrace); + Logs.error( + '[KeyManager] Failed to access online key backup: ' + + err.toString(), + stacktrace); } if (!hadPreviously && getInboundGroupSession(room.id, sessionId, senderKey) != null) { return; // we managed to load the session from online backup, no need to care about it now } } - // 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(); - final requestId = client.generateUniqueTransactionId(); - final request = KeyManagerKeyShareRequest( - requestId: requestId, - devices: devices, - room: room, - sessionId: sessionId, - senderKey: senderKey, - ); - await client.sendToDevice( - [], + 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(); + final requestId = client.generateUniqueTransactionId(); + final request = KeyManagerKeyShareRequest( + requestId: requestId, + devices: devices, + room: room, + sessionId: sessionId, + senderKey: senderKey, + ); + final userList = await room.requestParticipants(); + await client.sendToDevicesOfUserIds( + userList.map((u) => u.id).toSet(), 'm.room_key_request', { 'action': 'request', @@ -440,9 +449,14 @@ class KeyManager { 'request_id': requestId, 'requesting_device_id': client.deviceID, }, - encrypted: false, - toUsers: await room.requestParticipants()); - outgoingShareRequests[request.requestId] = request; + ); + outgoingShareRequests[request.requestId] = request; + } catch (e, s) { + Logs.error( + '[Key Manager] Sending key verification request failed: ' + + e.toString(), + s); + } } /// Handle an incoming to_device event that is related to key sharing @@ -453,27 +467,27 @@ class KeyManager { } if (event.content['action'] == 'request') { // we are *receiving* a request - print('[KeyManager] Received key sharing request...'); + Logs.info('[KeyManager] Received key sharing request...'); if (!event.content.containsKey('body')) { - print('[KeyManager] No body, doing nothing'); + Logs.info('[KeyManager] No body, doing nothing'); return; // no body } if (!client.userDeviceKeys.containsKey(event.sender) || !client.userDeviceKeys[event.sender].deviceKeys .containsKey(event.content['requesting_device_id'])) { - print('[KeyManager] Device not found, doing nothing'); + Logs.info('[KeyManager] Device not found, doing nothing'); return; // device not found } final device = client.userDeviceKeys[event.sender] .deviceKeys[event.content['requesting_device_id']]; if (device.userId == client.userID && device.deviceId == client.deviceID) { - print('[KeyManager] Request is by ourself, ignoring'); + Logs.info('[KeyManager] Request is by ourself, ignoring'); return; // ignore requests by ourself } final room = client.getRoomById(event.content['body']['room_id']); if (room == null) { - print('[KeyManager] Unknown room, ignoring'); + Logs.info('[KeyManager] Unknown room, ignoring'); return; // unknown room } final sessionId = event.content['body']['session_id']; @@ -481,7 +495,7 @@ class KeyManager { // okay, let's see if we have this session at all if ((await loadInboundGroupSession(room.id, sessionId, senderKey)) == null) { - print('[KeyManager] Unknown session, ignoring'); + Logs.info('[KeyManager] Unknown session, ignoring'); return; // we don't have this session anyways } final request = KeyManagerKeyShareRequest( @@ -492,7 +506,7 @@ class KeyManager { senderKey: senderKey, ); if (incomingShareRequests.containsKey(request.requestId)) { - print('[KeyManager] Already processed this request, ignoring'); + Logs.info('[KeyManager] Already processed this request, ignoring'); return; // we don't want to process one and the same request multiple times } incomingShareRequests[request.requestId] = request; @@ -501,11 +515,12 @@ class KeyManager { if (device.userId == client.userID && device.verified && !device.blocked) { - print('[KeyManager] All checks out, forwarding key...'); + Logs.info('[KeyManager] All checks out, forwarding key...'); // alright, we can forward the key await roomKeyRequest.forwardKey(); } else { - print('[KeyManager] Asking client, if the key should be forwarded'); + Logs.info( + '[KeyManager] Asking client, if the key should be forwarded'); client.onRoomKeyRequest .add(roomKeyRequest); // let the client handle this } @@ -553,15 +568,24 @@ class KeyManager { if (request.devices.isEmpty) { return; // no need to send any cancellation } + // Send with send-to-device messaging + final sendToDeviceMessage = { + 'action': 'request_cancellation', + 'request_id': request.requestId, + 'requesting_device_id': client.deviceID, + }; + var data = >>{}; + for (final device in request.devices) { + if (!data.containsKey(device.userId)) { + data[device.userId] = {}; + } + data[device.userId][device.deviceId] = sendToDeviceMessage; + } await client.sendToDevice( - request.devices, - 'm.room_key_request', - { - 'action': 'request_cancellation', - 'request_id': request.requestId, - 'requesting_device_id': client.deviceID, - }, - encrypted: false); + 'm.room_key_request', + client.generateUniqueTransactionId(), + data, + ); } else if (event.type == 'm.room_key') { if (event.encryptedContent == null) { return; // the event wasn't encrypted, this is a security risk; @@ -660,7 +684,7 @@ class RoomKeyRequest extends ToDeviceEvent { message['session_key'] = session.inboundGroupSession .export_session(session.inboundGroupSession.first_known_index()); // send the actual reply of the key back to the requester - await keyManager.client.sendToDevice( + await keyManager.client.sendToDeviceEncrypted( [requestingDevice], 'm.forwarded_room_key', message, diff --git a/lib/encryption/key_verification_manager.dart b/lib/encryption/key_verification_manager.dart index de82074..d02d107 100644 --- a/lib/encryption/key_verification_manager.dart +++ b/lib/encryption/key_verification_manager.dart @@ -67,6 +67,10 @@ class KeyVerificationManager { if (_requests.containsKey(transactionId)) { await _requests[transactionId].handlePayload(event.type, event.content); } else { + if (!['m.key.verification.request', 'm.key.verification.start'] + .contains(event.type)) { + return; // we can only start on these + } final newKeyRequest = KeyVerification(encryption: encryption, userId: event.sender); await newKeyRequest.handlePayload(event.type, event.content); @@ -111,6 +115,10 @@ class KeyVerificationManager { _requests.remove(transactionId); } } else if (event['sender'] != client.userID) { + if (!['m.key.verification.request', 'm.key.verification.start'] + .contains(type)) { + return; // we can only start on these + } final room = client.getRoomById(update.roomID) ?? Room(id: update.roomID, client: client); final newKeyRequest = KeyVerification( diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index 743e432..6bb9493 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -18,6 +18,7 @@ import 'dart:convert'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:pedantic/pedantic.dart'; import 'package:canonical_json/canonical_json.dart'; import 'package:famedlysdk/famedlysdk.dart'; @@ -119,9 +120,9 @@ class OlmManager { try { olmutil.ed25519_verify(key, message, signature); isValid = true; - } catch (e) { + } catch (e, s) { isValid = false; - print('[LibOlm] Signature check failed: ' + e.toString()); + Logs.error('[LibOlm] Signature check failed: ' + e.toString(), s); } finally { olmutil.free(); } @@ -182,7 +183,7 @@ class OlmManager { signJson(keysContent['device_keys'] as Map); } - final response = await client.api.uploadDeviceKeys( + final response = await client.uploadDeviceKeys( deviceKeys: uploadDeviceKeys ? MatrixDeviceKeys.fromJson(keysContent['device_keys']) : null, @@ -231,7 +232,7 @@ class OlmManager { return event; } if (event.content['algorithm'] != 'm.olm.v1.curve25519-aes-sha2') { - throw ('Unknown algorithm: ${event.content}'); + throw ('Unknown algorithm: ${event.content['algorithm']}'); } if (!event.content['ciphertext'].containsKey(identityKey)) { throw ("The message isn't sent for this device"); @@ -334,7 +335,7 @@ class OlmManager { return; } await startOutgoingOlmSessions([device]); - await client.sendToDevice([device], 'm.dummy', {}); + await client.sendToDeviceEncrypted([device], 'm.dummy', {}); } Future decryptToDeviceEvent(ToDeviceEvent event) async { @@ -382,7 +383,7 @@ class OlmManager { } final response = - await client.api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000); + await client.requestOneTimeKeys(requestingKeysFrom, timeout: 10000); for (var userKeysEntry in response.oneTimeKeys.entries) { final userId = userKeysEntry.key; @@ -408,10 +409,12 @@ class OlmManager { lastReceived: DateTime.now(), // we want to use a newly created session )); - } catch (e) { + } catch (e, s) { session.free(); - print('[LibOlm] Could not create new outbound olm session: ' + - e.toString()); + Logs.error( + '[LibOlm] Could not create new outbound olm session: ' + + e.toString(), + s); } } } @@ -483,8 +486,9 @@ class OlmManager { try { data[device.userId][device.deviceId] = await encryptToDeviceMessagePayload(device, type, payload); - } catch (e) { - print('[LibOlm] Error encrypting to-device event: ' + e.toString()); + } catch (e, s) { + Logs.error( + '[LibOlm] Error encrypting to-device event: ' + e.toString(), s); continue; } } diff --git a/lib/encryption/ssss.dart b/lib/encryption/ssss.dart index 9349a14..fb53ae2 100644 --- a/lib/encryption/ssss.dart +++ b/lib/encryption/ssss.dart @@ -22,6 +22,7 @@ import 'dart:convert'; import 'package:encrypt/encrypt.dart'; import 'package:crypto/crypto.dart'; import 'package:base58check/base58.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:password_hash/password_hash.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; @@ -221,7 +222,7 @@ class SSSS { 'mac': encrypted.mac, }; // store the thing in your account data - await client.api.setAccountData(client.userID, type, content); + await client.setAccountData(client.userID, type, content); if (CACHE_TYPES.contains(type) && client.database != null) { // cache the thing await client.database @@ -253,14 +254,14 @@ class SSSS { Future request(String type, List devices) async { // only send to own, verified devices - print('[SSSS] Requesting type ${type}...'); + Logs.info('[SSSS] Requesting type ${type}...'); devices.removeWhere((DeviceKeys d) => d.userId != client.userID || !d.verified || d.blocked || d.deviceId == client.deviceID); if (devices.isEmpty) { - print('[SSSS] Warn: No devices'); + Logs.warning('[SSSS] No devices'); return; } final requestId = client.generateUniqueTransactionId(); @@ -270,7 +271,7 @@ class SSSS { devices: devices, ); pendingShareRequests[requestId] = request; - await client.sendToDevice(devices, 'm.secret.request', { + await client.sendToDeviceEncrypted(devices, 'm.secret.request', { 'action': 'request', 'requesting_device_id': client.deviceID, 'request_id': requestId, @@ -281,32 +282,33 @@ class SSSS { Future handleToDeviceEvent(ToDeviceEvent event) async { if (event.type == 'm.secret.request') { // got a request to share a secret - print('[SSSS] Received sharing request...'); + Logs.info('[SSSS] Received sharing request...'); if (event.sender != client.userID || !client.userDeviceKeys.containsKey(client.userID)) { - print('[SSSS] Not sent by us'); + Logs.info('[SSSS] Not sent by us'); return; // we aren't asking for it ourselves, so ignore } if (event.content['action'] != 'request') { - print('[SSSS] it is actually a cancelation'); + Logs.info('[SSSS] it is actually a cancelation'); return; // not actually requesting, so ignore } final device = client.userDeviceKeys[client.userID] .deviceKeys[event.content['requesting_device_id']]; if (device == null || !device.verified || device.blocked) { - print('[SSSS] Unknown / unverified devices, ignoring'); + Logs.info('[SSSS] Unknown / unverified devices, ignoring'); return; // nope....unknown or untrusted device } // alright, all seems fine...let's check if we actually have the secret they are asking for final type = event.content['name']; final secret = await getCached(type); if (secret == null) { - print('[SSSS] We don\'t have the secret for ${type} ourself, ignoring'); + Logs.info( + '[SSSS] We don\'t have the secret for ${type} ourself, ignoring'); return; // seems like we don't have this, either } // okay, all checks out...time to share this secret! - print('[SSSS] Replying with secret for ${type}'); - await client.sendToDevice( + Logs.info('[SSSS] Replying with secret for ${type}'); + await client.sendToDeviceEncrypted( [device], 'm.secret.send', { @@ -315,11 +317,11 @@ class SSSS { }); } else if (event.type == 'm.secret.send') { // receiving a secret we asked for - print('[SSSS] Received shared secret...'); + Logs.info('[SSSS] Received shared secret...'); if (event.sender != client.userID || !pendingShareRequests.containsKey(event.content['request_id']) || event.encryptedContent == null) { - print('[SSSS] Not by us or unknown request'); + Logs.info('[SSSS] Not by us or unknown request'); return; // we have no idea what we just received } final request = pendingShareRequests[event.content['request_id']]; @@ -330,26 +332,26 @@ class SSSS { d.curve25519Key == event.encryptedContent['sender_key'], orElse: () => null); if (device == null) { - print('[SSSS] Someone else replied?'); + Logs.info('[SSSS] Someone else replied?'); return; // someone replied whom we didn't send the share request to } final secret = event.content['secret']; if (!(event.content['secret'] is String)) { - print('[SSSS] Secret wasn\'t a string'); + Logs.info('[SSSS] Secret wasn\'t a string'); return; // the secret wasn't a string....wut? } // let's validate if the secret is, well, valid if (_validators.containsKey(request.type) && !(await _validators[request.type](secret))) { - print('[SSSS] The received secret was invalid'); + Logs.info('[SSSS] The received secret was invalid'); return; // didn't pass the validator } pendingShareRequests.remove(request.requestId); if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) { - print('[SSSS] Request is too far in the past'); + Logs.info('[SSSS] Request is too far in the past'); return; // our request is more than 15min in the past...better not trust it anymore } - print('[SSSS] Secret for type ${request.type} is ok, storing it'); + Logs.info('[SSSS] Secret for type ${request.type} is ok, storing it'); if (client.database != null) { final keyId = keyIdFromType(request.type); if (keyId != null) { diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index e4a43f3..4b89f06 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -19,6 +19,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:canonical_json/canonical_json.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:pedantic/pedantic.dart'; import 'package:olm/olm.dart' as olm; import 'package:famedlysdk/famedlysdk.dart'; @@ -150,7 +151,7 @@ class KeyVerification { } void dispose() { - print('[Key Verification] disposing object...'); + Logs.info('[Key Verification] disposing object...'); method?.dispose(); } @@ -202,7 +203,8 @@ class KeyVerification { await Future.delayed(Duration(milliseconds: 50)); } _handlePayloadLock = true; - print('[Key Verification] Received type ${type}: ' + payload.toString()); + Logs.info( + '[Key Verification] Received type ${type}: ' + payload.toString()); try { var thisLastStep = lastStep; switch (type) { @@ -215,7 +217,10 @@ class KeyVerification { DateTime.fromMillisecondsSinceEpoch(payload['timestamp']); if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) || now.add(Duration(minutes: 5)).isBefore(verifyTime)) { - await cancel('m.timeout'); + // if the request is more than 20min in the past we just silently fail it + // to not generate too many cancels + await cancel('m.timeout', + now.subtract(Duration(minutes: 20)).isAfter(verifyTime)); return; } // verify it has a method we can use @@ -280,6 +285,13 @@ class KeyVerification { } method = _makeVerificationMethod(payload['method'], this); if (lastStep == null) { + // validate the start time + if (room != null) { + // we just silently ignore in-room-verification starts + await cancel('m.unknown_method', true); + return; + } + // validate the specific payload if (!method.validateStart(payload)) { await cancel('m.unknown_method'); return; @@ -287,7 +299,7 @@ class KeyVerification { startPaylaod = payload; setState(KeyVerificationState.askAccept); } else { - print('handling start in method.....'); + Logs.info('handling start in method.....'); await method.handlePayload(type, payload); } break; @@ -301,18 +313,20 @@ class KeyVerification { setState(KeyVerificationState.error); break; default: - await method.handlePayload(type, payload); + if (method != null) { + await method.handlePayload(type, payload); + } else { + await cancel('m.invalid_message'); + } break; } if (lastStep == thisLastStep) { lastStep = type; } } catch (err, stacktrace) { - print('[Key Verification] An error occured: ' + err.toString()); - print(stacktrace); - if (deviceId != null) { - await cancel('m.invalid_message'); - } + Logs.error( + '[Key Verification] An error occured: ' + err.toString(), stacktrace); + await cancel('m.invalid_message'); } finally { _handlePayloadLock = false; } @@ -510,11 +524,13 @@ class KeyVerification { return false; } - Future cancel([String code = 'm.unknown']) async { - await send('m.key.verification.cancel', { - 'reason': code, - 'code': code, - }); + Future cancel([String code = 'm.unknown', bool quiet = false]) async { + if (!quiet && (deviceId != null || room != null)) { + await send('m.key.verification.cancel', { + 'reason': code, + 'code': code, + }); + } canceled = true; canceledCode = code; setState(KeyVerificationState.error); @@ -536,9 +552,10 @@ class KeyVerification { Future send(String type, Map payload) async { makePayload(payload); - print('[Key Verification] Sending type ${type}: ' + payload.toString()); + Logs.info('[Key Verification] Sending type ${type}: ' + payload.toString()); if (room != null) { - print('[Key Verification] Sending to ${userId} in room ${room.id}'); + Logs.info( + '[Key Verification] Sending to ${userId} in room ${room.id}...'); if (['m.key.verification.request'].contains(type)) { payload['msgtype'] = type; payload['to'] = userId; @@ -552,8 +569,9 @@ class KeyVerification { encryption.keyVerificationManager.addRequest(this); } } else { - print('[Key Verification] Sending to ${userId} device ${deviceId}'); - await client.sendToDevice( + Logs.info( + '[Key Verification] Sending to ${userId} device ${deviceId}...'); + await client.sendToDeviceEncrypted( [client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload); } } @@ -679,8 +697,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { break; } } catch (err, stacktrace) { - print('[Key Verification SAS] An error occured: ' + err.toString()); - print(stacktrace); + Logs.error('[Key Verification SAS] An error occured: ' + err.toString(), + stacktrace); if (request.deviceId != null) { await request.cancel('m.invalid_message'); } diff --git a/lib/encryption/utils/olm_session.dart b/lib/encryption/utils/olm_session.dart index 1dca413..73d8a98 100644 --- a/lib/encryption/utils/olm_session.dart +++ b/lib/encryption/utils/olm_session.dart @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:olm/olm.dart' as olm; import '../../src/database/database.dart' show DbOlmSessions; @@ -46,8 +47,8 @@ class OlmSession { lastReceived = dbEntry.lastReceived ?? DateTime.fromMillisecondsSinceEpoch(0); assert(sessionId == session.session_id()); - } catch (e) { - print('[LibOlm] Could not unpickle olm session: ' + e.toString()); + } catch (e, s) { + Logs.error('[LibOlm] Could not unpickle olm session: ' + e.toString(), s); dispose(); } } diff --git a/lib/encryption/utils/outbound_group_session.dart b/lib/encryption/utils/outbound_group_session.dart index 2a1c617..bf10818 100644 --- a/lib/encryption/utils/outbound_group_session.dart +++ b/lib/encryption/utils/outbound_group_session.dart @@ -18,6 +18,7 @@ import 'dart:convert'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:olm/olm.dart' as olm; import '../../src/database/database.dart' show DbOutboundGroupSession; @@ -44,10 +45,11 @@ class OutboundGroupSession { devices = List.from(json.decode(dbEntry.deviceIds)); creationTime = dbEntry.creationTime; sentMessages = dbEntry.sentMessages; - } catch (e) { + } catch (e, s) { dispose(); - print( - '[LibOlm] Unable to unpickle outboundGroupSession: ' + e.toString()); + Logs.error( + '[LibOlm] Unable to unpickle outboundGroupSession: ' + e.toString(), + s); } } diff --git a/lib/encryption/utils/session_key.dart b/lib/encryption/utils/session_key.dart index 5c6f0b2..176c9e0 100644 --- a/lib/encryption/utils/session_key.dart +++ b/lib/encryption/utils/session_key.dart @@ -18,6 +18,7 @@ import 'dart:convert'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:olm/olm.dart' as olm; import 'package:famedlysdk/famedlysdk.dart'; @@ -48,9 +49,11 @@ class SessionKey { inboundGroupSession = olm.InboundGroupSession(); try { inboundGroupSession.unpickle(key, dbEntry.pickle); - } catch (e) { + } catch (e, s) { dispose(); - print('[LibOlm] Unable to unpickle inboundGroupSession: ' + e.toString()); + Logs.error( + '[LibOlm] Unable to unpickle inboundGroupSession: ' + e.toString(), + s); } } diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index 39860ab..3a1c51d 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -28,6 +28,7 @@ export 'package:famedlysdk/src/utils/uri_extension.dart'; export 'package:famedlysdk/src/utils/matrix_localizations.dart'; export 'package:famedlysdk/src/utils/receipt.dart'; export 'package:famedlysdk/src/utils/states_map.dart'; +export 'package:famedlysdk/src/utils/sync_update_extension.dart'; export 'package:famedlysdk/src/utils/to_device_event.dart'; export 'package:famedlysdk/src/client.dart'; export 'package:famedlysdk/src/event.dart'; diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index 770dd25..50b66c0 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -88,9 +88,6 @@ class MatrixApi { /// timeout which is usually 30 seconds. int syncTimeoutSec; - /// Whether debug prints should be displayed. - final bool debug; - http.Client httpClient = http.Client(); bool get _testMode => @@ -101,7 +98,6 @@ class MatrixApi { MatrixApi({ this.homeserver, this.accessToken, - this.debug = false, http.Client httpClient, this.syncTimeoutSec = 30, }) { @@ -161,11 +157,6 @@ class MatrixApi { headers['Authorization'] = 'Bearer ${accessToken}'; } - if (debug) { - print( - '[REQUEST ${describeEnum(type)}] $action, Data: ${jsonEncode(data)}'); - } - http.Response resp; var jsonResp = {}; try { @@ -212,8 +203,6 @@ class MatrixApi { throw exception; } - - if (debug) print('[RESPONSE] ${jsonResp.toString()}'); _timeoutFactor = 1; } on TimeoutException catch (_) { _timeoutFactor *= 2; @@ -787,7 +776,7 @@ class MatrixApi { String stateKey = '', ]) async { final response = await request(RequestType.PUT, - '/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/state/${Uri.encodeQueryComponent(eventType)}/${Uri.encodeQueryComponent(stateKey)}', + '/client/r0/rooms/${Uri.encodeComponent(roomId)}/state/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(stateKey)}', data: content); return response['event_id']; } @@ -803,7 +792,7 @@ class MatrixApi { Map content, ) async { final response = await request(RequestType.PUT, - '/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/send/${Uri.encodeQueryComponent(eventType)}/${Uri.encodeQueryComponent(txnId)}', + '/client/r0/rooms/${Uri.encodeComponent(roomId)}/send/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(txnId)}', data: content); return response['event_id']; } @@ -818,7 +807,7 @@ class MatrixApi { String reason, }) async { final response = await request(RequestType.PUT, - '/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/redact/${Uri.encodeQueryComponent(eventId)}/${Uri.encodeQueryComponent(txnId)}', + '/client/r0/rooms/${Uri.encodeComponent(roomId)}/redact/${Uri.encodeComponent(eventId)}/${Uri.encodeComponent(txnId)}', data: { if (reason != null) 'reason': reason, }); @@ -1300,7 +1289,6 @@ class MatrixApi { streamedRequest.contentLength = await file.length; streamedRequest.sink.add(file); streamedRequest.sink.close(); - if (debug) print('[UPLOADING] $fileName'); var streamedResponse = _testMode ? null : await streamedRequest.send(); Map jsonResponse = json.decode( String.fromCharCodes(_testMode @@ -1341,8 +1329,11 @@ class MatrixApi { /// This endpoint is used to send send-to-device events to a set of client devices. /// https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-sendtodevice-eventtype-txnid - Future sendToDevice(String eventType, String txnId, - Map>> messages) async { + Future sendToDevice( + String eventType, + String txnId, + Map>> messages, + ) async { await request( RequestType.PUT, '/client/r0/sendToDevice/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(txnId)}', @@ -1734,7 +1725,7 @@ class MatrixApi { Future> requestRoomTags(String userId, String roomId) async { final response = await request( RequestType.GET, - '/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/tags', + '/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/tags', ); return (response['tags'] as Map).map( (k, v) => MapEntry(k, Tag.fromJson(v)), @@ -1750,7 +1741,7 @@ class MatrixApi { double order, }) async { await request(RequestType.PUT, - '/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/tags/${Uri.encodeQueryComponent(tag)}', + '/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/tags/${Uri.encodeComponent(tag)}', data: { if (order != null) 'order': order, }); @@ -1762,7 +1753,7 @@ class MatrixApi { Future removeRoomTag(String userId, String roomId, String tag) async { await request( RequestType.DELETE, - '/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/tags/${Uri.encodeQueryComponent(tag)}', + '/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/tags/${Uri.encodeComponent(tag)}', ); return; } @@ -1777,7 +1768,7 @@ class MatrixApi { ) async { await request( RequestType.PUT, - '/client/r0/user/${Uri.encodeQueryComponent(userId)}/account_data/${Uri.encodeQueryComponent(type)}', + '/client/r0/user/${Uri.encodeComponent(userId)}/account_data/${Uri.encodeComponent(type)}', data: content, ); return; @@ -1791,7 +1782,7 @@ class MatrixApi { ) async { return await request( RequestType.GET, - '/client/r0/user/${Uri.encodeQueryComponent(userId)}/account_data/${Uri.encodeQueryComponent(type)}', + '/client/r0/user/${Uri.encodeComponent(userId)}/account_data/${Uri.encodeComponent(type)}', ); } @@ -1806,7 +1797,7 @@ class MatrixApi { ) async { await request( RequestType.PUT, - '/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/account_data/${Uri.encodeQueryComponent(type)}', + '/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}', data: content, ); return; @@ -1821,7 +1812,7 @@ class MatrixApi { ) async { return await request( RequestType.GET, - '/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/account_data/${Uri.encodeQueryComponent(type)}', + '/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}', ); } @@ -1830,7 +1821,7 @@ class MatrixApi { Future requestWhoIsInfo(String userId) async { final response = await request( RequestType.GET, - '/client/r0/admin/whois/${Uri.encodeQueryComponent(userId)}', + '/client/r0/admin/whois/${Uri.encodeComponent(userId)}', ); return WhoIsInfo.fromJson(response); } @@ -1845,7 +1836,7 @@ class MatrixApi { String filter, }) async { final response = await request(RequestType.GET, - '/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/context/${Uri.encodeQueryComponent(eventId)}', + '/client/r0/rooms/${Uri.encodeComponent(roomId)}/context/${Uri.encodeComponent(eventId)}', query: { if (filter != null) 'filter': filter, if (limit != null) 'limit': limit.toString(), @@ -1862,7 +1853,7 @@ class MatrixApi { int score, ) async { await request(RequestType.POST, - '/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/report/${Uri.encodeQueryComponent(eventId)}', + '/client/r0/rooms/${Uri.encodeComponent(roomId)}/report/${Uri.encodeComponent(eventId)}', data: { 'reason': reason, 'score': score, diff --git a/lib/matrix_api/model/event_types.dart b/lib/matrix_api/model/event_types.dart index 4de7608..25f4585 100644 --- a/lib/matrix_api/model/event_types.dart +++ b/lib/matrix_api/model/event_types.dart @@ -19,6 +19,7 @@ abstract class EventTypes { static const String Message = 'm.room.message'; static const String Sticker = 'm.sticker'; + static const String Reaction = 'm.reaction'; static const String Redaction = 'm.room.redaction'; static const String RoomAliases = 'm.room.aliases'; static const String RoomCanonicalAlias = 'm.room.canonical_alias'; diff --git a/lib/matrix_api/model/message_types.dart b/lib/matrix_api/model/message_types.dart index db478bd..90fe2d0 100644 --- a/lib/matrix_api/model/message_types.dart +++ b/lib/matrix_api/model/message_types.dart @@ -25,7 +25,6 @@ abstract class MessageTypes { static const String Audio = 'm.audio'; static const String File = 'm.file'; static const String Location = 'm.location'; - static const String Reply = 'm.relates_to'; static const String Sticker = 'm.sticker'; static const String BadEncrypted = 'm.bad.encrypted'; static const String None = 'm.none'; diff --git a/lib/src/client.dart b/lib/src/client.dart index 740fa99..820ab3d 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -22,9 +22,9 @@ import 'dart:core'; import 'package:famedlysdk/encryption.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:famedlysdk/matrix_api.dart'; import 'package:famedlysdk/src/room.dart'; import 'package:famedlysdk/src/utils/device_keys_list.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart'; import 'package:famedlysdk/src/utils/to_device_event.dart'; import 'package:http/http.dart' as http; @@ -44,7 +44,7 @@ enum LoginState { logged, loggedOut } /// Represents a Matrix client to communicate with a /// [Matrix](https://matrix.org) homeserver and is the entry point for this /// SDK. -class Client { +class Client extends MatrixApi { int _id; int get id => _id; @@ -52,7 +52,8 @@ class Client { bool enableE2eeRecovery; - MatrixApi api; + @deprecated + MatrixApi get api => this; Encryption encryption; @@ -81,14 +82,16 @@ class Client { /// - m.room.canonical_alias /// - m.room.tombstone /// - *some* m.room.member events, where needed - Client(this.clientName, - {this.debug = false, - this.database, - this.enableE2eeRecovery = false, - this.verificationMethods, - http.Client httpClient, - this.importantStateEvents, - this.pinUnreadRooms = false}) { + Client( + this.clientName, { + this.database, + this.enableE2eeRecovery = false, + this.verificationMethods, + http.Client httpClient, + this.importantStateEvents, + this.pinUnreadRooms = false, + @deprecated bool debug, + }) { verificationMethods ??= {}; importantStateEvents ??= {}; importantStateEvents.addAll([ @@ -100,17 +103,9 @@ class Client { EventTypes.RoomCanonicalAlias, EventTypes.RoomTombstone, ]); - api = MatrixApi(debug: debug, httpClient: httpClient); - onLoginStateChanged.stream.listen((loginState) { - if (debug) { - print('[LoginState]: ${loginState.toString()}'); - } - }); + this.httpClient = httpClient; } - /// Whether debug prints should be displayed. - final bool debug; - /// The required name for this client. final String clientName; @@ -130,7 +125,7 @@ class Client { String _deviceName; /// Returns the current login state. - bool isLogged() => api.accessToken != null; + bool isLogged() => accessToken != null; /// A list of all rooms the user is participating or invited. List get rooms => _rooms; @@ -153,7 +148,7 @@ class Client { /// Warning! This endpoint is for testing only! set rooms(List newList) { - print('Warning! This endpoint is for testing only!'); + Logs.warning('Warning! This endpoint is for testing only!'); _rooms = newList; } @@ -165,21 +160,6 @@ class Client { int _transactionCounter = 0; - @Deprecated('Use [api.request()] instead') - Future> jsonRequest( - {RequestType type, - String action, - dynamic data = '', - int timeout, - String contentType = 'application/json'}) => - api.request( - type, - action, - data: data, - timeout: timeout, - contentType: contentType, - ); - String generateUniqueTransactionId() { _transactionCounter++; return '${clientName}-${_transactionCounter}-${DateTime.now().millisecondsSinceEpoch}'; @@ -260,8 +240,20 @@ class Client { /// Throws FormatException, TimeoutException and MatrixException on error. Future checkServer(dynamic serverUrl) async { try { - api.homeserver = (serverUrl is Uri) ? serverUrl : Uri.parse(serverUrl); - final versions = await api.requestSupportedVersions(); + if (serverUrl is Uri) { + homeserver = serverUrl; + } 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(); + // strip a trailing slash + if (serverUrl.endsWith('/')) { + serverUrl = serverUrl.substring(0, serverUrl.length - 1); + } + homeserver = Uri.parse(serverUrl); + } + final versions = await requestSupportedVersions(); for (var i = 0; i < versions.versions.length; i++) { if (versions.versions[i] == 'r0.5.0' || @@ -272,7 +264,7 @@ class Client { } } - final loginTypes = await api.requestLoginTypes(); + final loginTypes = await requestLoginTypes(); if (loginTypes.flows.indexWhere((f) => f.type == 'm.login.password') == -1) { return false; @@ -280,7 +272,7 @@ class Client { return true; } catch (_) { - api.homeserver = null; + homeserver = null; rethrow; } } @@ -288,16 +280,17 @@ class Client { /// 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. - Future register({ - String kind, + @override + Future register({ String username, String password, - Map auth, String deviceId, String initialDeviceDisplayName, bool inhibitLogin, + Map auth, + String kind, }) async { - final response = await api.register( + final response = await super.register( username: username, password: password, auth: auth, @@ -315,68 +308,64 @@ class Client { await connect( newToken: response.accessToken, newUserID: response.userId, - newHomeserver: api.homeserver, + newHomeserver: homeserver, newDeviceName: initialDeviceDisplayName ?? '', newDeviceID: response.deviceId); - return; + return response; } /// 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. - Future login( - String username, - String password, { - String initialDeviceDisplayName, + @override + Future login({ + String type = 'm.login.password', + String userIdentifierType = 'm.id.user', + String user, + String medium, + String address, + String password, + String token, String deviceId, + String initialDeviceDisplayName, }) async { - var data = { - 'type': 'm.login.password', - 'user': username, - 'identifier': { - 'type': 'm.id.user', - 'user': username, - }, - 'password': password, - }; - if (deviceId != null) data['device_id'] = deviceId; - if (initialDeviceDisplayName != null) { - data['initial_device_display_name'] = initialDeviceDisplayName; - } - - final loginResp = await api.login( - type: 'm.login.password', - userIdentifierType: 'm.id.user', - user: username, + final loginResp = await super.login( + type: type, + userIdentifierType: userIdentifierType, + user: user, password: password, deviceId: deviceId, initialDeviceDisplayName: initialDeviceDisplayName, + medium: medium, + address: address, + token: token, ); // Connect if there is an access token in the response. if (loginResp.accessToken == null || loginResp.deviceId == null || loginResp.userId == null) { - throw 'Registered but token, device ID or user ID is null.'; + throw Exception('Registered but token, device ID or user ID is null.'); } await connect( newToken: loginResp.accessToken, newUserID: loginResp.userId, - newHomeserver: api.homeserver, + newHomeserver: homeserver, newDeviceName: initialDeviceDisplayName ?? '', newDeviceID: loginResp.deviceId, ); - return true; + return loginResp; } /// Sends a logout command to the homeserver and clears all local data, /// including all persistent data from the store. + @override Future logout() async { try { - await api.logout(); - } catch (exception) { - print(exception); + await super.logout(); + } catch (e, s) { + Logs.error(e, s); rethrow; } finally { await clear(); @@ -427,19 +416,19 @@ class Client { if (cache && _profileCache.containsKey(userId)) { return _profileCache[userId]; } - final profile = await api.requestProfile(userId); + final profile = await requestProfile(userId); _profileCache[userId] = profile; return profile; } Future> get archive async { var archiveList = []; - final sync = await api.sync( + final syncResp = await sync( filter: '{"room":{"include_leave":true,"timeline":{"limit":10}}}', timeout: 0, ); - if (sync.rooms.leave is Map) { - for (var entry in sync.rooms.leave.entries) { + if (syncResp.rooms.leave is Map) { + for (var entry in syncResp.rooms.leave.entries) { final id = entry.key; final room = entry.value; var leftRoom = Room( @@ -466,14 +455,10 @@ class Client { return archiveList; } - /// Changes the user's displayname. - Future setDisplayname(String displayname) => - api.setDisplayname(userID, displayname); - /// Uploads a new user avatar for this user. Future setAvatar(MatrixFile file) async { - final uploadResp = await api.upload(file.bytes, file.name); - await api.setAvatarUrl(userID, Uri.parse(uploadResp)); + final uploadResp = await upload(file.bytes, file.name); + await setAvatarUrl(userID, Uri.parse(uploadResp)); return; } @@ -556,10 +541,6 @@ class Client { final StreamController onKeyVerificationRequest = StreamController.broadcast(); - /// Matrix synchronisation is done with https long polling. This needs a - /// timeout which is usually 30 seconds. - int syncTimeoutSec = 30; - /// How long should the app wait until it retrys the synchronisation after /// an error? int syncErrorTimeoutSec = 3; @@ -581,7 +562,7 @@ class Client { /// "type": "m.login.password", /// "user": "test", /// "password": "1234", - /// "initial_device_display_name": "Fluffy Matrix Client" + /// "initial_device_display_name": "Matrix Client" /// }); /// ``` /// @@ -610,8 +591,8 @@ class Client { final account = await database.getClient(clientName); if (account != null) { _id = account.clientId; - api.homeserver = Uri.parse(account.homeserverUrl); - api.accessToken = account.token; + homeserver = Uri.parse(account.homeserverUrl); + accessToken = account.token; _userID = account.userId; _deviceID = account.deviceId; _deviceName = account.deviceName; @@ -619,15 +600,15 @@ class Client { olmAccount = account.olmAccount; } } - api.accessToken = newToken ?? api.accessToken; - api.homeserver = newHomeserver ?? api.homeserver; + accessToken = newToken ?? accessToken; + homeserver = newHomeserver ?? homeserver; _userID = newUserID ?? _userID; _deviceID = newDeviceID ?? _deviceID; _deviceName = newDeviceName ?? _deviceName; prevBatch = newPrevBatch ?? prevBatch; olmAccount = newOlmAccount ?? olmAccount; - if (api.accessToken == null || api.homeserver == null || _userID == null) { + if (accessToken == null || homeserver == null || _userID == null) { // we aren't logged in encryption?.dispose(); encryption = null; @@ -635,15 +616,15 @@ class Client { return; } - encryption = Encryption( - debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery); + encryption = + Encryption(client: this, enableE2eeRecovery: enableE2eeRecovery); await encryption.init(olmAccount); if (database != null) { if (id != null) { await database.updateClient( - api.homeserver.toString(), - api.accessToken, + homeserver.toString(), + accessToken, _userID, _deviceID, _deviceName, @@ -654,8 +635,8 @@ class Client { } else { _id = await database.insertClient( clientName, - api.homeserver.toString(), - api.accessToken, + homeserver.toString(), + accessToken, _userID, _deviceID, _deviceName, @@ -671,6 +652,9 @@ class Client { } onLoginStateChanged.add(LoginState.logged); + Logs.success( + 'Successfully connected as ${userID.localpart} with ${homeserver.toString()}', + ); return _sync(); } @@ -683,8 +667,8 @@ class Client { /// Resets all settings and stops the synchronisation. void clear() { database?.clear(id); - _id = api.accessToken = - api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null; + _id = accessToken = + homeserver = _userID = _deviceID = _deviceName = prevBatch = null; _rooms = []; encryption?.dispose(); encryption = null; @@ -697,13 +681,11 @@ class Client { Future _sync() async { if (isLogged() == false || _disposed) return; try { - _syncRequest = api - .sync( + _syncRequest = sync( filter: syncFilters, since: prevBatch, timeout: prevBatch != null ? 30000 : null, - ) - .catchError((e) { + ).catchError((e) { _lastSyncError = e; return null; }); @@ -741,8 +723,7 @@ class Client { if (isLogged() == false || _disposed) { return; } - print('Error during processing events: ' + e.toString()); - print(s); + Logs.error('Error during processing events: ' + e.toString(), s); onSyncError.add(SyncError( exception: e is Exception ? e : Exception(e), stackTrace: s)); await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync); @@ -821,10 +802,10 @@ class Client { try { toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent); } catch (e, s) { - print( - '[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}'); - print(e); - print(s); + Logs.error( + '[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}\n${e.toString()}', + s); + onOlmError.add( ToDeviceEventDecryptionError( exception: e is Exception ? e : Exception(e), @@ -1160,11 +1141,16 @@ class Client { var userIds = {}; for (var i = 0; i < rooms.length; i++) { if (rooms[i].encrypted) { - var userList = await rooms[i].requestParticipants(); - for (var user in userList) { - if ([Membership.join, Membership.invite].contains(user.membership)) { - userIds.add(user.id); + try { + var userList = await rooms[i].requestParticipants(); + for (var user in userList) { + if ([Membership.join, Membership.invite] + .contains(user.membership)) { + userIds.add(user.id); + } } + } catch (e, s) { + Logs.error('[E2EE] Failed to fetch participants: ' + e.toString(), s); } } } @@ -1196,8 +1182,7 @@ class Client { if (outdatedLists.isNotEmpty) { // Request the missing device key lists from the server. - final response = - await api.requestDeviceKeys(outdatedLists, timeout: 10000); + final response = await requestDeviceKeys(outdatedLists, timeout: 10000); for (final rawDeviceKeyListEntry in response.deviceKeys.entries) { final userId = rawDeviceKeyListEntry.key; @@ -1332,27 +1317,49 @@ class Client { } } } - await database?.transaction(() async { - for (final f in dbActions) { - await f(); - } - }); - } catch (e) { - print('[LibOlm] Unable to update user device keys: ' + e.toString()); + + if (dbActions.isNotEmpty) { + await database?.transaction(() async { + for (final f in dbActions) { + await f(); + } + }); + } + } catch (e, s) { + Logs.error( + '[LibOlm] Unable to update user device keys: ' + e.toString(), s); } } + /// Send an (unencrypted) to device [message] of a specific [eventType] to all + /// devices of a set of [users]. + Future sendToDevicesOfUserIds( + Set users, + String eventType, + Map message, { + String messageId, + }) async { + // Send with send-to-device messaging + var data = >>{}; + for (var user in users) { + data[user] = {}; + data[user]['*'] = message; + } + await sendToDevice( + eventType, messageId ?? generateUniqueTransactionId(), data); + return; + } + /// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send /// the request to all devices of the current user, pass an empty list to [deviceKeys]. - Future sendToDevice( + Future sendToDeviceEncrypted( List deviceKeys, - String type, + String eventType, Map message, { - bool encrypted = true, - List toUsers, + String messageId, bool onlyVerified = false, }) async { - if (encrypted && !encryptionEnabled) return; + if (!encryptionEnabled) return; // Don't send this message to blocked devices, and if specified onlyVerified // then only send it to verified devices if (deviceKeys.isNotEmpty) { @@ -1363,36 +1370,13 @@ class Client { if (deviceKeys.isEmpty) return; } - var sendToDeviceMessage = message; - // Send with send-to-device messaging var data = >>{}; - if (deviceKeys.isEmpty) { - if (toUsers == null) { - data[userID] = {}; - data[userID]['*'] = sendToDeviceMessage; - } else { - for (var user in toUsers) { - data[user.id] = {}; - data[user.id]['*'] = sendToDeviceMessage; - } - } - } else { - if (encrypted) { - data = - await encryption.encryptToDeviceMessage(deviceKeys, type, message); - } else { - for (final device in deviceKeys) { - if (!data.containsKey(device.userId)) { - data[device.userId] = {}; - } - data[device.userId][device.deviceId] = sendToDeviceMessage; - } - } - } - if (encrypted) type = EventTypes.Encrypted; - final messageID = generateUniqueTransactionId(); - await api.sendToDevice(type, messageID, data); + data = + await encryption.encryptToDeviceMessage(deviceKeys, eventType, message); + eventType = EventTypes.Encrypted; + await sendToDevice( + eventType, messageId ?? generateUniqueTransactionId(), data); } /// Whether all push notifications are muted using the [.m.rule.master] @@ -1417,7 +1401,7 @@ class Client { } Future setMuteAllPushNotifications(bool muted) async { - await api.enablePushRule( + await enablePushRule( 'global', PushRuleKind.override, '.m.rule.master', @@ -1427,6 +1411,7 @@ class Client { } /// Changes the password. You should either set oldPasswort or another authentication flow. + @override Future changePassword(String newPassword, {String oldPassword, Map auth}) async { try { @@ -1437,7 +1422,7 @@ class Client { 'password': oldPassword, }; } - await api.changePassword(newPassword, auth: auth); + await super.changePassword(newPassword, auth: auth); } on MatrixException catch (matrixException) { if (!matrixException.requireAdditionalAuthentication) { rethrow; diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index d339336..793fd48 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -1,3 +1,4 @@ +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:moor/moor.dart'; import 'dart:convert'; @@ -6,6 +7,7 @@ import 'package:famedlysdk/matrix_api.dart' as api; import 'package:olm/olm.dart' as olm; import '../../matrix_api.dart'; +import '../room.dart'; part 'database.g.dart'; @@ -65,7 +67,7 @@ class Database extends _$Database { if (executor.dialect == SqlDialect.sqlite) { final ret = await customSelect('PRAGMA journal_mode=WAL').get(); if (ret.isNotEmpty) { - print('[Moor] Switched database to mode ' + + Logs.info('[Moor] Switched database to mode ' + ret.first.data['journal_mode'].toString()); } } @@ -112,8 +114,9 @@ class Database extends _$Database { 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()); + } catch (e, s) { + Logs.error( + '[LibOlm] Could not unpickle olm session: ' + e.toString(), s); } } return res; @@ -357,13 +360,40 @@ class Database extends _$Database { if (type == 'timeline' || type == 'history') { // calculate the status var status = 2; + if (eventContent['unsigned'] is Map && + eventContent['unsigned'][MessageSendingStatusKey] is num) { + status = eventContent['unsigned'][MessageSendingStatusKey]; + } if (eventContent['status'] is num) status = eventContent['status']; if ((status == 1 || status == -1) && eventContent['unsigned'] is Map && 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); + final allOldEvents = + await getEvent(clientId, eventContent['event_id'], chatId).get(); + if (allOldEvents.isNotEmpty) { + // we were likely unable to change transaction_id -> event_id.....because the event ID already exists! + // So, we try to fetch the old event + // the transaction id event will automatically be deleted further down + final oldEvent = allOldEvents.first; + // do we update the status? We should allow 0 -> -1 updates and status increases + if (status > oldEvent.status || + (oldEvent.status == 0 && status == -1)) { + // update the status + await updateEventStatusOnly( + status, clientId, eventContent['event_id'], chatId); + } + } else { + // status changed and we have an old transaction id --> update event id and stuffs + try { + await updateEventStatus(status, eventContent['event_id'], clientId, + eventContent['unsigned']['transaction_id'], chatId); + } catch (err) { + // we could not update the transaction id to the event id....so it already exists + // as we just tried to fetch the event previously this is a race condition if the event comes down sync in the mean time + // that means that the status we already have in the database is likely more accurate + // than our status. So, we just ignore this error + } + } } else { DbEvent oldEvent; if (type == 'history') { diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index 203313f..6c3849c 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -6033,6 +6033,21 @@ abstract class _$Database extends GeneratedDatabase { ); } + Future updateEventStatusOnly( + int status, int client_id, String event_id, String room_id) { + return customUpdate( + 'UPDATE events SET status = :status WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id', + variables: [ + Variable.withInt(status), + Variable.withInt(client_id), + Variable.withString(event_id), + Variable.withString(room_id) + ], + updates: {events}, + updateKind: UpdateKind.update, + ); + } + DbRoomState _rowToDbRoomState(QueryRow row) { return DbRoomState( clientId: row.readInt('client_id'), diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index cade18c..dbcb632 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -208,6 +208,7 @@ 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); 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; +updateEventStatusOnly: UPDATE events SET status = :status WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id; getImportantRoomStates: SELECT * FROM room_states WHERE client_id = :client_id AND type IN :events; getAllRoomStates: SELECT * FROM room_states WHERE client_id = :client_id; getUnimportantRoomStatesForRoom: SELECT * FROM room_states WHERE client_id = :client_id AND room_id = :room_id AND type NOT IN :events; diff --git a/lib/src/event.dart b/lib/src/event.dart index 50c3563..23c53ca 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -20,6 +20,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/encryption.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:famedlysdk/src/utils/receipt.dart'; import 'package:http/http.dart' as http; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; @@ -28,6 +29,12 @@ import './room.dart'; import 'utils/matrix_localizations.dart'; import './database/database.dart' show DbRoomState, DbEvent; +abstract class RelationshipTypes { + static const String Reply = 'm.in_reply_to'; + static const String Edit = 'm.replace'; + static const String Reaction = 'm.annotation'; +} + /// 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 extends MatrixEvent { User get sender => room.getUserByMXIDSync(senderId ?? '@unknown'); @@ -90,12 +97,18 @@ class Event extends MatrixEvent { this.senderId = senderId; this.unsigned = unsigned; // synapse unfortunatley isn't following the spec and tosses the prev_content - // into the unsigned block - this.prevContent = prevContent != null && prevContent.isNotEmpty - ? prevContent - : (unsigned != null && unsigned['prev_content'] is Map - ? unsigned['prev_content'] - : null); + // into the unsigned block. + // Currently we are facing a very strange bug in web which is impossible to debug. + // It may be because of this line so we put this in try-catch until we can fix it. + try { + this.prevContent = (prevContent != null && prevContent.isNotEmpty) + ? prevContent + : (unsigned != null && unsigned['prev_content'] is Map) + ? unsigned['prev_content'] + : null; + } catch (e, s) { + Logs.error('Event constructor crashed: ${e.toString()}', s); + } this.stateKey = stateKey; this.originServerTs = originServerTs; } @@ -140,7 +153,9 @@ class Event extends MatrixEvent { final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']); final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']); return Event( - status: jsonPayload['status'] ?? defaultStatus, + status: jsonPayload['status'] ?? + unsigned[MessageSendingStatusKey] ?? + defaultStatus, stateKey: jsonPayload['state_key'], prevContent: prevContent, content: content, @@ -212,10 +227,7 @@ class Event extends MatrixEvent { unsigned: unsigned, room: room); - String get messageType => (content['m.relates_to'] is Map && - content['m.relates_to']['m.in_reply_to'] != null) - ? MessageTypes.Reply - : content['msgtype'] ?? MessageTypes.Text; + String get messageType => content['msgtype'] ?? MessageTypes.Text; void setRedactionEvent(Event redactedBecause) { unsigned = { @@ -312,12 +324,13 @@ class Event extends MatrixEvent { /// Try to send this event again. Only works with events of status -1. Future sendAgain({String txid}) async { if (status != -1) return null; - await remove(); - final eventID = await room.sendEvent( + // we do not remove the event here. It will automatically be updated + // in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2 + final newEventId = await room.sendEvent( content, - txid: txid ?? unsigned['transaction_id'], + txid: txid ?? unsigned['transaction_id'] ?? eventId, ); - return eventID; + return newEventId; } /// Whether the client is allowed to redact this event. @@ -327,20 +340,10 @@ class Event extends MatrixEvent { Future redact({String reason, String txid}) => room.redactEvent(eventId, reason: reason, txid: txid); - /// Whether this event is in reply to another event. - bool get isReply => - content['m.relates_to'] is Map && - content['m.relates_to']['m.in_reply_to'] is Map && - content['m.relates_to']['m.in_reply_to']['event_id'] is String && - (content['m.relates_to']['m.in_reply_to']['event_id'] as String) - .isNotEmpty; - /// Searches for the reply event in the given timeline. Future getReplyEvent(Timeline timeline) async { - if (!isReply) return null; - final String replyEventId = - content['m.relates_to']['m.in_reply_to']['event_id']; - return await timeline.getEventById(replyEventId); + if (relationshipType != RelationshipTypes.Reply) return null; + return await timeline.getEventById(relationshipEventId); } /// If this event is encrypted and the decryption was not successful because @@ -480,9 +483,8 @@ class Event extends MatrixEvent { final targetName = stateKeyUser.calcDisplayname(); // Has the membership changed? final newMembership = content['membership'] ?? ''; - final oldMembership = unsigned['prev_content'] is Map - ? unsigned['prev_content']['membership'] ?? '' - : ''; + final oldMembership = + prevContent != null ? prevContent['membership'] ?? '' : ''; if (newMembership != oldMembership) { if (oldMembership == 'invite' && newMembership == 'join') { text = i18n.acceptedTheInvitation(targetName); @@ -517,15 +519,12 @@ class Event extends MatrixEvent { } } else if (newMembership == 'join') { final newAvatar = content['avatar_url'] ?? ''; - final oldAvatar = unsigned['prev_content'] is Map - ? unsigned['prev_content']['avatar_url'] ?? '' - : ''; + final oldAvatar = + prevContent != null ? prevContent['avatar_url'] ?? '' : ''; final newDisplayname = content['displayname'] ?? ''; final oldDisplayname = - unsigned['prev_content'] is Map - ? unsigned['prev_content']['displayname'] ?? '' - : ''; + prevContent != null ? prevContent['displayname'] ?? '' : ''; // Has the user avatar changed? if (newAvatar != oldAvatar) { @@ -631,7 +630,6 @@ class Event extends MatrixEvent { case MessageTypes.Text: case MessageTypes.Notice: case MessageTypes.None: - case MessageTypes.Reply: localizedBody = body; break; } @@ -660,9 +658,85 @@ class Event extends MatrixEvent { static const Set textOnlyMessageTypes = { MessageTypes.Text, - MessageTypes.Reply, MessageTypes.Notice, MessageTypes.Emote, MessageTypes.None, }; + + /// returns if this event matches the passed event or transaction id + bool matchesEventOrTransactionId(String search) { + if (search == null) { + return false; + } + if (eventId == search) { + return true; + } + return unsigned != null && unsigned['transaction_id'] == search; + } + + /// Get the relationship type of an event. `null` if there is none + String get relationshipType { + if (content == null || !(content['m.relates_to'] is Map)) { + return null; + } + if (content['m.relates_to'].containsKey('rel_type')) { + return content['m.relates_to']['rel_type']; + } + if (content['m.relates_to'].containsKey('m.in_reply_to')) { + return RelationshipTypes.Reply; + } + return null; + } + + /// Get the event ID that this relationship will reference. `null` if there is none + String get relationshipEventId { + if (content == null || !(content['m.relates_to'] is Map)) { + return null; + } + if (content['m.relates_to'].containsKey('event_id')) { + return content['m.relates_to']['event_id']; + } + if (content['m.relates_to']['m.in_reply_to'] is Map && + content['m.relates_to']['m.in_reply_to'].containsKey('event_id')) { + return content['m.relates_to']['m.in_reply_to']['event_id']; + } + return null; + } + + /// Get wether this event has aggregated events from a certain [type] + /// To be able to do that you need to pass a [timeline] + bool hasAggregatedEvents(Timeline timeline, String type) => + timeline.aggregatedEvents.containsKey(eventId) && + timeline.aggregatedEvents[eventId].containsKey(type); + + /// Get all the aggregated event objects for a given [type]. To be able to do this + /// you have to pass a [timeline] + Set aggregatedEvents(Timeline timeline, String type) => + hasAggregatedEvents(timeline, type) + ? timeline.aggregatedEvents[eventId][type] + : {}; + + /// Fetches the event to be rendered, taking into account all the edits and the like. + /// It needs a [timeline] for that. + Event getDisplayEvent(Timeline timeline) { + if (hasAggregatedEvents(timeline, RelationshipTypes.Edit)) { + // alright, we have an edit + final allEditEvents = aggregatedEvents(timeline, RelationshipTypes.Edit) + // we only allow edits made by the original author themself + .where((e) => e.senderId == senderId && e.type == EventTypes.Message) + .toList(); + // we need to check again if it isn't empty, as we potentially removed all + // aggregated edits + if (allEditEvents.isNotEmpty) { + allEditEvents.sort((a, b) => a.sortOrder - b.sortOrder > 0 ? 1 : -1); + var rawEvent = allEditEvents.last.toJson(); + // update the content of the new event to render + if (rawEvent['content']['m.new_content'] is Map) { + rawEvent['content'] = rawEvent['content']['m.new_content']; + } + return Event.fromJson(rawEvent, room); + } + } + return this; + } } diff --git a/lib/src/room.dart b/lib/src/room.dart index 0113123..f8d25c1 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -23,6 +23,7 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/client.dart'; import 'package:famedlysdk/src/event.dart'; import 'package:famedlysdk/src/utils/event_update.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:famedlysdk/src/utils/room_update.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; @@ -39,6 +40,8 @@ enum PushRuleState { notify, mentions_only, dont_notify } enum JoinRules { public, knock, invite, private } enum GuestAccess { can_join, forbidden } enum HistoryVisibility { invited, joined, shared, world_readable } +const String MessageSendingStatusKey = + 'com.famedly.famedlysdk.message_sending_status'; /// Represents a Matrix room. class Room { @@ -104,7 +107,9 @@ class Room { /// Flag if the room is partial, meaning not all state events have been loaded yet bool partial = true; - /// Load all the missing state events for the room from the database. If the room has already been loaded, this does nothing. + /// Post-loads the room. + /// This load all the missing state events for the room from the database + /// If the room has already been loaded, this does nothing. Future postLoad() async { if (!partial || client.database == null) { return; @@ -132,8 +137,8 @@ class Room { if (state.type == EventTypes.Encrypted && client.encryptionEnabled) { try { state = client.encryption.decryptRoomEventSync(id, state); - } catch (e) { - print('[LibOlm] Could not decrypt room state: ' + e.toString()); + } catch (e, s) { + Logs.error('[LibOlm] Could not decrypt room state: ' + e.toString(), s); } } if (!(state.stateKey is String) && @@ -369,21 +374,21 @@ class Room { /// Call the Matrix API to change the name of this room. Returns the event ID of the /// new m.room.name event. - Future setName(String newName) => client.api.sendState( + Future setName(String newName) => client.sendState( id, EventTypes.RoomName, {'name': newName}, ); /// Call the Matrix API to change the topic of this room. - Future setDescription(String newName) => client.api.sendState( + Future setDescription(String newName) => client.sendState( id, EventTypes.RoomTopic, {'topic': newName}, ); /// Add a tag to the room. - Future addTag(String tag, {double order}) => client.api.addRoomTag( + Future addTag(String tag, {double order}) => client.addRoomTag( client.userID, id, tag, @@ -391,7 +396,7 @@ class Room { ); /// Removes a tag from the room. - Future removeTag(String tag) => client.api.removeRoomTag( + Future removeTag(String tag) => client.removeRoomTag( client.userID, id, tag, @@ -418,7 +423,7 @@ class Room { /// Call the Matrix API to change the pinned events of this room. Future setPinnedEvents(List pinnedEventIds) => - client.api.sendState( + client.sendState( id, EventTypes.RoomPinnedEvents, {'pinned': pinnedEventIds}, @@ -500,6 +505,7 @@ class Room { Future sendTextEvent(String message, {String txid, Event inReplyTo, + String editEventId, bool parseMarkdown = true, Map> emotePacks}) { final event = { @@ -518,7 +524,20 @@ class Room { event['formatted_body'] = html; } } - return sendEvent(event, txid: txid, inReplyTo: inReplyTo); + return sendEvent(event, + txid: txid, inReplyTo: inReplyTo, editEventId: editEventId); + } + + /// Sends a reaction to an event with an [eventId] and the content [key] into a room. + /// Returns the event ID generated by the server for this reaction. + Future sendReaction(String eventId, String key, {String txid}) { + return sendEvent({ + 'm.relates_to': { + 'rel_type': RelationshipTypes.Reaction, + 'event_id': eventId, + 'key': key, + }, + }, type: EventTypes.Reaction, txid: txid); } /// Sends a [file] to this room after uploading it. Returns the mxc uri of @@ -529,6 +548,7 @@ class Room { MatrixFile file, { String txid, Event inReplyTo, + String editEventId, bool waitUntilSent = false, MatrixImageFile thumbnail, }) async { @@ -545,13 +565,13 @@ class Room { uploadThumbnail = encryptedThumbnail.toMatrixFile(); } } - final uploadResp = await client.api.upload( + final uploadResp = await client.upload( uploadFile.bytes, uploadFile.name, contentType: uploadFile.mimeType, ); final thumbnailUploadResp = uploadThumbnail != null - ? await client.api.upload( + ? await client.upload( uploadThumbnail.bytes, uploadThumbnail.name, contentType: uploadThumbnail.mimeType, @@ -605,6 +625,7 @@ class Room { content, txid: txid, inReplyTo: inReplyTo, + editEventId: editEventId, ); if (waitUntilSent) { await sendResponse; @@ -615,7 +636,7 @@ class Room { /// Sends an event to this room with this json as a content. Returns the /// event ID generated from the server. Future sendEvent(Map content, - {String type, String txid, Event inReplyTo}) async { + {String type, String txid, Event inReplyTo, String editEventId}) async { type = type ?? EventTypes.Message; final sendType = (encrypted && client.encryptionEnabled) ? EventTypes.Encrypted : type; @@ -645,28 +666,38 @@ class Room { }, }; } + if (editEventId != null) { + final newContent = Map.from(content); + content['m.new_content'] = newContent; + content['m.relates_to'] = { + 'event_id': editEventId, + 'rel_type': RelationshipTypes.Edit, + }; + if (content['body'] is String) { + content['body'] = '* ' + content['body']; + } + if (content['formatted_body'] is String) { + content['formatted_body'] = '* ' + content['formatted_body']; + } + } - final sortOrder = newSortOrder; - // Display a *sending* event and store it. - var eventUpdate = EventUpdate( - type: 'timeline', - roomID: id, - eventType: type, - sortOrder: sortOrder, - content: { - 'type': type, - 'event_id': messageID, - 'sender': client.userID, - 'status': 0, - 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, - 'content': content - }, - ); - client.onEvent.add(eventUpdate); - await client.database?.transaction(() async { - await client.database.storeEventUpdate(client.id, eventUpdate); - await updateSortOrder(); - }); + final syncUpdate = SyncUpdate() + ..rooms = (RoomsUpdate() + ..join = ({}..[id] = (JoinedRoomUpdate() + ..timeline = (TimelineUpdate() + ..events = [ + MatrixEvent() + ..content = content + ..type = type + ..eventId = messageID + ..senderId = client.userID + ..originServerTs = DateTime.now() + ..unsigned = { + MessageSendingStatusKey: 0, + 'transaction_id': messageID, + }, + ])))); + await client.handleSync(syncUpdate); // Send the text and on success, store and display a *sent* event. try { @@ -674,29 +705,23 @@ class Room { ? await client.encryption .encryptGroupMessagePayload(id, content, type: type) : content; - final res = await client.api.sendMessage( + final res = await client.sendMessage( id, sendType, messageID, sendMessageContent, ); - eventUpdate.content['status'] = 1; - eventUpdate.content['unsigned'] = {'transaction_id': messageID}; - eventUpdate.content['event_id'] = res; - client.onEvent.add(eventUpdate); - await client.database?.transaction(() async { - await client.database.storeEventUpdate(client.id, eventUpdate); - }); + syncUpdate.rooms.join.values.first.timeline.events.first + .unsigned[MessageSendingStatusKey] = 1; + syncUpdate.rooms.join.values.first.timeline.events.first.eventId = res; + await client.handleSync(syncUpdate); return res; - } catch (exception) { - print('[Client] Error while sending: ' + exception.toString()); - // On error, set status to -1 - eventUpdate.content['status'] = -1; - eventUpdate.content['unsigned'] = {'transaction_id': messageID}; - client.onEvent.add(eventUpdate); - await client.database?.transaction(() async { - await client.database.storeEventUpdate(client.id, eventUpdate); - }); + } catch (e, s) { + Logs.warning( + '[Client] Problem while sending message: ' + e.toString(), s); + syncUpdate.rooms.join.values.first.timeline.events.first + .unsigned[MessageSendingStatusKey] = -1; + await client.handleSync(syncUpdate); } return null; } @@ -706,7 +731,7 @@ class Room { /// automatically be set. Future join() async { try { - await client.api.joinRoom(id); + await client.joinRoom(id); final invitation = getState(EventTypes.RoomMember, client.userID); if (invitation != null && invitation.content['is_direct'] is bool && @@ -732,25 +757,25 @@ class Room { /// chat, this will be removed too. Future leave() async { if (directChatMatrixID != '') await removeFromDirectChat(); - await client.api.leaveRoom(id); + await client.leaveRoom(id); return; } /// Call the Matrix API to forget this room if you already left it. Future forget() async { await client.database?.forgetRoom(client.id, id); - await client.api.forgetRoom(id); + await client.forgetRoom(id); return; } /// Call the Matrix API to kick a user from this room. - Future kick(String userID) => client.api.kickFromRoom(id, userID); + Future kick(String userID) => client.kickFromRoom(id, userID); /// Call the Matrix API to ban a user from this room. - Future ban(String userID) => client.api.banFromRoom(id, userID); + Future ban(String userID) => client.banFromRoom(id, userID); /// Call the Matrix API to unban a banned user from this room. - Future unban(String userID) => client.api.unbanInRoom(id, userID); + Future unban(String userID) => client.unbanInRoom(id, userID); /// Set the power level of the user with the [userID] to the value [power]. /// Returns the event ID of the new state event. If there is no known @@ -762,7 +787,7 @@ class Room { if (powerMap['users'] == null) powerMap['users'] = {}; powerMap['users'][userID] = power; - return await client.api.sendState( + return await client.sendState( id, EventTypes.RoomPowerLevels, powerMap, @@ -770,14 +795,14 @@ class Room { } /// Call the Matrix API to invite a user to this room. - Future invite(String userID) => client.api.inviteToRoom(id, userID); + Future invite(String userID) => client.inviteToRoom(id, userID); /// Request more previous events from the server. [historyCount] defines how much events should /// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before** /// the historical events will be published in the onEvent stream. Future requestHistory( {int historyCount = DefaultHistoryCount, onHistoryReceived}) async { - final resp = await client.api.requestMessages( + final resp = await client.requestMessages( id, prev_batch, Direction.b, @@ -828,7 +853,7 @@ class Room { directChats[userID] = [id]; } - await client.api.setAccountData( + await client.setAccountData( client.userID, 'm.direct', directChats, @@ -846,7 +871,7 @@ class Room { return; } // Nothing to do here - await client.api.setRoomAccountData( + await client.setRoomAccountData( client.userID, id, 'm.direct', @@ -859,7 +884,7 @@ class Room { Future sendReadReceipt(String eventID) async { notificationCount = 0; await client.database?.resetNotificationCount(client.id, id); - await client.api.sendReadMarker( + await client.sendReadMarker( id, eventID, readReceiptLocationEventId: eventID, @@ -992,7 +1017,7 @@ class Room { } } if (participantListComplete) return getParticipants(); - final matrixEvents = await client.api.requestMembers(id); + final matrixEvents = await client.requestMembers(id); final users = matrixEvents.map((e) => Event.fromMatrixEvent(e, this).asUser).toList(); for (final user in users) { @@ -1055,7 +1080,7 @@ class Room { if (mxID == null || !_requestingMatrixIds.add(mxID)) return null; Map resp; try { - resp = await client.api.requestStateContent( + resp = await client.requestStateContent( id, EventTypes.RoomMember, mxID, @@ -1068,7 +1093,7 @@ class Room { } if (resp == null && requestProfile) { try { - final profile = await client.api.requestProfile(mxID); + final profile = await client.requestProfile(mxID); resp = { 'displayname': profile.displayname, 'avatar_url': profile.avatarUrl, @@ -1110,7 +1135,7 @@ class Room { /// Searches for the event on the server. Returns null if not found. Future getEventById(String eventID) async { - final matrixEvent = await client.api.requestEvent(id, eventID); + final matrixEvent = await client.requestEvent(id, eventID); return Event.fromMatrixEvent(matrixEvent, this); } @@ -1144,8 +1169,8 @@ class Room { /// Uploads a new user avatar for this room. Returns the event ID of the new /// m.room.avatar event. Future setAvatar(MatrixFile file) async { - final uploadResp = await client.api.upload(file.bytes, file.name); - return await client.api.sendState( + final uploadResp = await client.upload(file.bytes, file.name); + return await client.sendState( id, EventTypes.RoomAvatar, {'url': uploadResp}, @@ -1242,23 +1267,23 @@ class Room { // All push notifications should be sent to the user case PushRuleState.notify: if (pushRuleState == PushRuleState.dont_notify) { - await client.api.deletePushRule('global', PushRuleKind.override, id); + await client.deletePushRule('global', PushRuleKind.override, id); } else if (pushRuleState == PushRuleState.mentions_only) { - await client.api.deletePushRule('global', PushRuleKind.room, id); + await client.deletePushRule('global', PushRuleKind.room, id); } break; // Only when someone mentions the user, a push notification should be sent case PushRuleState.mentions_only: if (pushRuleState == PushRuleState.dont_notify) { - await client.api.deletePushRule('global', PushRuleKind.override, id); - await client.api.setPushRule( + await client.deletePushRule('global', PushRuleKind.override, id); + await client.setPushRule( 'global', PushRuleKind.room, id, [PushRuleAction.dont_notify], ); } else if (pushRuleState == PushRuleState.notify) { - await client.api.setPushRule( + await client.setPushRule( 'global', PushRuleKind.room, id, @@ -1269,9 +1294,9 @@ class Room { // No push notification should be ever sent for this room. case PushRuleState.dont_notify: if (pushRuleState == PushRuleState.mentions_only) { - await client.api.deletePushRule('global', PushRuleKind.room, id); + await client.deletePushRule('global', PushRuleKind.room, id); } - await client.api.setPushRule( + await client.setPushRule( 'global', PushRuleKind.override, id, @@ -1297,7 +1322,7 @@ class Room { } var data = {}; if (reason != null) data['reason'] = reason; - return await client.api.redact( + return await client.redact( id, eventId, messageID, @@ -1310,7 +1335,7 @@ class Room { 'typing': isTyping, }; if (timeout != null) data['timeout'] = timeout; - return client.api.sendTypingNotification(client.userID, id, isTyping); + return client.sendTypingNotification(client.userID, id, isTyping); } /// This is sent by the caller when they wish to establish a call. @@ -1324,7 +1349,7 @@ class Room { {String type = 'offer', int version = 0, String txid}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; - return await client.api.sendMessage( + return await client.sendMessage( id, EventTypes.CallInvite, txid, @@ -1362,7 +1387,7 @@ class Room { String txid, }) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; - return await client.api.sendMessage( + return await client.sendMessage( id, EventTypes.CallCandidates, txid, @@ -1382,7 +1407,7 @@ class Room { Future answerCall(String callId, String sdp, {String type = 'answer', int version = 0, String txid}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; - return await client.api.sendMessage( + return await client.sendMessage( id, EventTypes.CallAnswer, txid, @@ -1400,7 +1425,7 @@ class Room { Future hangupCall(String callId, {int version = 0, String txid}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; - return await client.api.sendMessage( + return await client.sendMessage( id, EventTypes.CallHangup, txid, @@ -1436,7 +1461,7 @@ class Room { /// Changes the join rules. You should check first if the user is able to change it. Future setJoinRules(JoinRules joinRules) async { - await client.api.sendState( + await client.sendState( id, EventTypes.RoomJoinRules, { @@ -1461,7 +1486,7 @@ class Room { /// Changes the guest access. You should check first if the user is able to change it. Future setGuestAccess(GuestAccess guestAccess) async { - await client.api.sendState( + await client.sendState( id, EventTypes.GuestAccess, { @@ -1487,7 +1512,7 @@ class Room { /// Changes the history visibility. You should check first if the user is able to change it. Future setHistoryVisibility(HistoryVisibility historyVisibility) async { - await client.api.sendState( + await client.sendState( id, EventTypes.HistoryVisibility, { @@ -1514,7 +1539,7 @@ class Room { Future enableEncryption({int algorithmIndex = 0}) async { if (encrypted) throw ('Encryption is already enabled!'); final algorithm = Client.supportedGroupEncryptionAlgorithms[algorithmIndex]; - await client.api.sendState( + await client.sendState( id, EventTypes.Encryption, { diff --git a/lib/src/timeline.dart b/lib/src/timeline.dart index e219733..18fe0bf 100644 --- a/lib/src/timeline.dart +++ b/lib/src/timeline.dart @@ -19,6 +19,7 @@ import 'dart:async'; import 'package:famedlysdk/matrix_api.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'event.dart'; import 'room.dart'; @@ -35,6 +36,9 @@ class Timeline { final Room room; List events = []; + /// Map of event ID to map of type to set of aggregated events + Map>> aggregatedEvents = {}; + final onTimelineUpdateCallback onUpdate; final onTimelineInsertCallback onInsert; @@ -66,7 +70,10 @@ class Timeline { await room.requestHistory( historyCount: historyCount, onHistoryReceived: () { - if (room.prev_batch.isEmpty || room.prev_batch == null) events = []; + if (room.prev_batch.isEmpty || room.prev_batch == null) { + events.clear(); + aggregatedEvents.clear(); + } }, ); await Future.delayed(const Duration(seconds: 2)); @@ -82,9 +89,17 @@ class Timeline { // to be received via the onEvent stream, it is unneeded to call sortAndUpdate roomSub ??= room.client.onRoomUpdate.stream .where((r) => r.id == room.id && r.limitedTimeline == true) - .listen((r) => events.clear()); + .listen((r) { + events.clear(); + aggregatedEvents.clear(); + }); sessionIdReceivedSub ??= room.onSessionKeyReceived.stream.listen(_sessionKeyReceived); + + // we want to populate our aggregated events + for (final e in events) { + addAggregatedEvent(e); + } } /// Don't forget to call this before you dismiss this object! @@ -122,33 +137,97 @@ class Timeline { } int _findEvent({String event_id, String unsigned_txid}) { + // we want to find any existing event where either the passed event_id or the passed unsigned_txid + // matches either the event_id or transaction_id of the existing event. + // For that we create two sets, searchNeedle, what we search, and searchHaystack, where we check if there is a match. + // Now, after having these two sets, if the intersect between them is non-empty, we know that we have at least one match in one pair, + // thus meaning we found our element. + final searchNeedle = {}; + if (event_id != null) { + searchNeedle.add(event_id); + } + if (unsigned_txid != null) { + searchNeedle.add(unsigned_txid); + } int i; for (i = 0; i < events.length; i++) { - if (events[i].eventId == event_id || - (unsigned_txid != null && events[i].eventId == unsigned_txid)) break; + final searchHaystack = {}; + if (events[i].eventId != null) { + searchHaystack.add(events[i].eventId); + } + if (events[i].unsigned != null && + events[i].unsigned['transaction_id'] != null) { + searchHaystack.add(events[i].unsigned['transaction_id']); + } + if (searchNeedle.intersection(searchHaystack).isNotEmpty) { + break; + } } return i; } + void _removeEventFromSet(Set eventSet, Event event) { + eventSet.removeWhere((e) => + e.matchesEventOrTransactionId(event.eventId) || + (event.unsigned != null && + e.matchesEventOrTransactionId(event.unsigned['transaction_id']))); + } + + void addAggregatedEvent(Event event) { + // we want to add an event to the aggregation tree + if (event.relationshipType == null || event.relationshipEventId == null) { + return; // nothing to do + } + if (!aggregatedEvents.containsKey(event.relationshipEventId)) { + aggregatedEvents[event.relationshipEventId] = >{}; + } + if (!aggregatedEvents[event.relationshipEventId] + .containsKey(event.relationshipType)) { + aggregatedEvents[event.relationshipEventId] + [event.relationshipType] = {}; + } + // remove a potential old event + _removeEventFromSet( + aggregatedEvents[event.relationshipEventId][event.relationshipType], + event); + // add the new one + aggregatedEvents[event.relationshipEventId][event.relationshipType] + .add(event); + } + + void removeAggregatedEvent(Event event) { + aggregatedEvents.remove(event.eventId); + if (event.unsigned != null) { + aggregatedEvents.remove(event.unsigned['transaction_id']); + } + for (final types in aggregatedEvents.values) { + for (final events in types.values) { + _removeEventFromSet(events, event); + } + } + } + void _handleEventUpdate(EventUpdate eventUpdate) async { try { if (eventUpdate.roomID != room.id) return; if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') { + var status = eventUpdate.content['status'] ?? 2; // Redaction events are handled as modification for existing events. if (eventUpdate.eventType == EventTypes.Redaction) { final eventId = _findEvent(event_id: eventUpdate.content['redacts']); - if (eventId != null) { + if (eventId < events.length) { + removeAggregatedEvent(events[eventId]); events[eventId].setRedactionEvent(Event.fromJson( eventUpdate.content, room, eventUpdate.sortOrder)); } - } else if (eventUpdate.content['status'] == -2) { + } else if (status == -2) { var i = _findEvent(event_id: eventUpdate.content['event_id']); - if (i < events.length) events.removeAt(i); - } - // Is this event already in the timeline? - else if (eventUpdate.content['unsigned'] is Map && - eventUpdate.content['unsigned']['transaction_id'] is String) { + if (i < events.length) { + removeAggregatedEvent(events[i]); + events.removeAt(i); + } + } else { var i = _findEvent( event_id: eventUpdate.content['event_id'], unsigned_txid: eventUpdate.content['unsigned'] is Map @@ -156,41 +235,36 @@ class Timeline { : null); if (i < events.length) { + // we want to preserve the old sort order final tempSortOrder = events[i].sortOrder; + // if the old status is larger than the new one, we also want to preserve the old status + final oldStatus = events[i].status; events[i] = Event.fromJson( eventUpdate.content, room, eventUpdate.sortOrder); events[i].sortOrder = tempSortOrder; + // do we preserve the status? we should allow 0 -> -1 updates and status increases + if (status < oldStatus && !(status == -1 && oldStatus == 0)) { + events[i].status = oldStatus; + } + addAggregatedEvent(events[i]); + } else { + var newEvent = Event.fromJson( + eventUpdate.content, room, eventUpdate.sortOrder); + + if (eventUpdate.type == 'history' && + events.indexWhere( + (e) => e.eventId == eventUpdate.content['event_id']) != + -1) return; + + events.insert(0, newEvent); + addAggregatedEvent(newEvent); + if (onInsert != null) onInsert(0); } - } else { - Event newEvent; - var senderUser = room - .getState( - EventTypes.RoomMember, eventUpdate.content['sender']) - ?.asUser ?? - await room.client.database?.getUser( - room.client.id, eventUpdate.content['sender'], room); - if (senderUser != null) { - eventUpdate.content['displayname'] = senderUser.displayName; - eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString(); - } - - newEvent = - Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder); - - if (eventUpdate.type == 'history' && - events.indexWhere( - (e) => e.eventId == eventUpdate.content['event_id']) != - -1) return; - - events.insert(0, newEvent); - if (onInsert != null) onInsert(0); } } sortAndUpdate(); - } catch (e) { - if (room.client.debug) { - print('[WARNING] (_handleEventUpdate) ${e.toString()}'); - } + } catch (e, s) { + Logs.warning('Handle event update failed: ${e.toString()}', s); } } diff --git a/lib/src/user.dart b/lib/src/user.dart index 3efedf4..ea1ce42 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -146,7 +146,7 @@ class User extends Event { if (roomID != null) return roomID; // Start a new direct chat - final newRoomID = await room.client.api.createRoom( + final newRoomID = await room.client.createRoom( invite: [id], isDirect: true, preset: CreateRoomPreset.trusted_private_chat, diff --git a/lib/src/utils/event_update.dart b/lib/src/utils/event_update.dart index 0be1c4e..514966b 100644 --- a/lib/src/utils/event_update.dart +++ b/lib/src/utils/event_update.dart @@ -18,6 +18,7 @@ import '../../famedlysdk.dart'; import '../../matrix_api.dart'; +import 'logs.dart'; /// Represents a new event (e.g. a message in a room) or an update for an /// already known event. @@ -57,8 +58,8 @@ class EventUpdate { content: decrpytedEvent.toJson(), sortOrder: sortOrder, ); - } catch (e) { - print('[LibOlm] Could not decrypt megolm event: ' + e.toString()); + } catch (e, s) { + Logs.error('[LibOlm] Could not decrypt megolm event: ' + e.toString(), s); return this; } } diff --git a/lib/src/utils/logs.dart b/lib/src/utils/logs.dart new file mode 100644 index 0000000..f774de3 --- /dev/null +++ b/lib/src/utils/logs.dart @@ -0,0 +1,30 @@ +import 'package:ansicolor/ansicolor.dart'; + +abstract class Logs { + static final AnsiPen _infoPen = AnsiPen()..blue(); + static final AnsiPen _warningPen = AnsiPen()..yellow(); + static final AnsiPen _successPen = AnsiPen()..green(); + static final AnsiPen _errorPen = AnsiPen()..red(); + + static const String _prefixText = '[Famedly Matrix SDK] '; + + static void info(dynamic info) => print( + _prefixText + _infoPen(info.toString()), + ); + + static void success(dynamic obj, [dynamic stackTrace]) => print( + _prefixText + _successPen(obj.toString()), + ); + + static void warning(dynamic warning, [dynamic stackTrace]) => print( + _prefixText + + _warningPen(warning.toString()) + + (stackTrace != null ? '\n${stackTrace.toString()}' : ''), + ); + + static void error(dynamic obj, [dynamic stackTrace]) => print( + _prefixText + + _errorPen(obj.toString()) + + (stackTrace != null ? '\n${stackTrace.toString()}' : ''), + ); +} diff --git a/lib/src/utils/markdown.dart b/lib/src/utils/markdown.dart index b294473..082513b 100644 --- a/lib/src/utils/markdown.dart +++ b/lib/src/utils/markdown.dart @@ -65,6 +65,7 @@ class EmoteSyntax extends InlineSyntax { return true; } final element = Element.empty('img'); + element.attributes['data-mx-emote'] = ''; element.attributes['src'] = htmlEscape.convert(mxc); element.attributes['alt'] = htmlEscape.convert(emote); element.attributes['title'] = htmlEscape.convert(emote); diff --git a/lib/src/utils/matrix_file.dart b/lib/src/utils/matrix_file.dart index 1f72c95..f5561bc 100644 --- a/lib/src/utils/matrix_file.dart +++ b/lib/src/utils/matrix_file.dart @@ -1,6 +1,7 @@ /// Workaround until [File] in dart:io and dart:html is unified import 'dart:typed_data'; +import 'package:famedlysdk/matrix_api/model/message_types.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import 'package:mime/mime.dart'; @@ -22,7 +23,18 @@ class MatrixFile { int get size => bytes.length; - String get msgType => 'm.file'; + String get msgType { + if (mimeType.toLowerCase().startsWith('image/')) { + return MessageTypes.Image; + } + if (mimeType.toLowerCase().startsWith('video/')) { + return MessageTypes.Video; + } + if (mimeType.toLowerCase().startsWith('audio/')) { + return MessageTypes.Audio; + } + return MessageTypes.File; + } Map get info => ({ 'mimetype': mimeType, diff --git a/lib/src/utils/sync_update_extension.dart b/lib/src/utils/sync_update_extension.dart new file mode 100644 index 0000000..c4b9ecb --- /dev/null +++ b/lib/src/utils/sync_update_extension.dart @@ -0,0 +1,44 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:famedlysdk/matrix_api.dart'; + +/// This extension adds easy-to-use filters for the sync update, meant to be used on the `client.onSync` stream, e.g. +/// `client.onSync.stream.where((s) => s.hasRoomUpdate)`. Multiple filters can easily be +/// combind with boolean logic: `client.onSync.stream.where((s) => s.hasRoomUpdate || s.hasPresenceUpdate)` +extension SyncUpdateFilters on SyncUpdate { + /// Returns true if this sync updat has a room update + /// That means there is account data, if there is a room in one of the `join`, `leave` or `invite` blocks of the sync or if there is a to_device event. + bool get hasRoomUpdate { + // if we have an account data change we need to re-render, as `m.direct` might have changed + if (accountData?.isNotEmpty ?? false) { + return true; + } + // check for a to_device event + if (toDevice?.isNotEmpty ?? false) { + return true; + } + // return if there are rooms to update + return (rooms?.join?.isNotEmpty ?? false) || + (rooms?.invite?.isNotEmpty ?? false) || + (rooms?.leave?.isNotEmpty ?? false); + } + + /// Returns if this sync update has presence updates + bool get hasPresenceUpdate => presence != null && presence.isNotEmpty; +} diff --git a/lib/src/utils/uri_extension.dart b/lib/src/utils/uri_extension.dart index edecdcf..804e385 100644 --- a/lib/src/utils/uri_extension.dart +++ b/lib/src/utils/uri_extension.dart @@ -22,8 +22,8 @@ import 'dart:core'; extension MxcUriExtension on Uri { /// Returns a download Link to this content. String getDownloadLink(Client matrix) => isScheme('mxc') - ? matrix.api.homeserver != null - ? '${matrix.api.homeserver.toString()}/_matrix/media/r0/download/$host$path' + ? matrix.homeserver != null + ? '${matrix.homeserver.toString()}/_matrix/media/r0/download/$host$path' : '' : toString(); @@ -36,8 +36,8 @@ extension MxcUriExtension on Uri { final methodStr = method.toString().split('.').last; width = width.round(); height = height.round(); - return matrix.api.homeserver != null - ? '${matrix.api.homeserver.toString()}/_matrix/media/r0/thumbnail/$host$path?width=$width&height=$height&method=$methodStr' + return matrix.homeserver != null + ? '${matrix.homeserver.toString()}/_matrix/media/r0/thumbnail/$host$path?width=$width&height=$height&method=$methodStr' : ''; } } diff --git a/pubspec.lock b/pubspec.lock index faec33f..25d83fe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.2" + ansicolor: + dependency: "direct main" + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" args: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a82843e..d594d4c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: password_hash: ^2.0.0 olm: ^1.2.1 matrix_file_e2ee: ^1.0.4 + ansicolor: ^1.0.2 dev_dependencies: test: ^1.0.0 diff --git a/test/client_test.dart b/test/client_test.dart index 41a9fab..2fa4178 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -23,6 +23,7 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; import 'package:famedlysdk/src/client.dart'; import 'package:famedlysdk/src/utils/event_update.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:famedlysdk/src/utils/room_update.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart'; import 'package:olm/olm.dart' as olm; @@ -45,10 +46,10 @@ void main() { const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo'; /// All Tests related to the Login - group('FluffyMatrix', () { + group('Client', () { /// Check if all Elements get created - matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + matrix = Client('testclient', httpClient: FakeMatrixApi()); roomUpdateListFuture = matrix.onRoomUpdate.stream.toList(); eventUpdateListFuture = matrix.onEvent.stream.toList(); @@ -59,9 +60,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); test('Login', () async { var presenceCounter = 0; @@ -73,7 +74,7 @@ void main() { accountDataCounter++; }); - expect(matrix.api.homeserver, null); + expect(matrix.homeserver, null); try { await matrix.checkServer('https://fakeserver.wrongaddress'); @@ -81,17 +82,9 @@ void main() { expect(exception != null, true); } await matrix.checkServer('https://fakeserver.notexisting'); - expect( - matrix.api.homeserver.toString(), 'https://fakeserver.notexisting'); + expect(matrix.homeserver.toString(), 'https://fakeserver.notexisting'); - final resp = await matrix.api.login( - type: 'm.login.password', - user: 'test', - password: '1234', - initialDeviceDisplayName: 'Fluffy Matrix Client', - ); - - final available = await matrix.api.usernameAvailable('testuser'); + final available = await matrix.usernameAvailable('testuser'); expect(available, true); var loginStateFuture = matrix.onLoginStateChanged.stream.first; @@ -99,21 +92,16 @@ void main() { var syncFuture = matrix.onSync.stream.first; matrix.connect( - newToken: resp.accessToken, - newUserID: resp.userId, - newHomeserver: matrix.api.homeserver, + newToken: 'abcd', + newUserID: '@test:fakeServer.notExisting', + newHomeserver: matrix.homeserver, newDeviceName: 'Text Matrix Client', - newDeviceID: resp.deviceId, + newDeviceID: 'GHTYAJCE', newOlmAccount: pickledOlmAccount, ); await Future.delayed(Duration(milliseconds: 50)); - expect(matrix.api.accessToken == resp.accessToken, true); - expect(matrix.deviceName == 'Text Matrix Client', true); - expect(matrix.deviceID == resp.deviceId, true); - expect(matrix.userID == resp.userId, true); - var loginState = await loginStateFuture; var firstSync = await firstSyncFuture; var sync = await syncFuture; @@ -207,14 +195,11 @@ void main() { }); test('Logout', () async { - await matrix.api.logout(); - var loginStateFuture = matrix.onLoginStateChanged.stream.first; + await matrix.logout(); - matrix.clear(); - - expect(matrix.api.accessToken == null, true); - expect(matrix.api.homeserver == null, true); + expect(matrix.accessToken == null, true); + expect(matrix.homeserver == null, true); expect(matrix.userID == null, true); expect(matrix.deviceID == null, true); expect(matrix.deviceName == null, true); @@ -322,17 +307,17 @@ void main() { }); test('Login', () async { - matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + matrix = Client('testclient', httpClient: FakeMatrixApi()); roomUpdateListFuture = matrix.onRoomUpdate.stream.toList(); eventUpdateListFuture = matrix.onEvent.stream.toList(); final checkResp = await matrix.checkServer('https://fakeServer.notExisting'); - final loginResp = await matrix.login('test', '1234'); + final loginResp = await matrix.login(user: 'test', password: '1234'); expect(checkResp, true); - expect(loginResp, true); + expect(loginResp != null, true); }); test('setAvatar', () async { @@ -385,8 +370,8 @@ void main() { } } }, matrix); - test('sendToDevice', () async { - await matrix.sendToDevice( + test('sendToDeviceEncrypted', () async { + await matrix.sendToDeviceEncrypted( [deviceKeys], 'm.message', { @@ -395,8 +380,7 @@ void main() { }); }); test('Test the fake store api', () async { - var client1 = - Client('testclient', debug: true, httpClient: FakeMatrixApi()); + var client1 = Client('testclient', httpClient: FakeMatrixApi()); client1.database = getDatabase(); client1.connect( @@ -413,17 +397,16 @@ void main() { expect(client1.isLogged(), true); expect(client1.rooms.length, 2); - var client2 = - Client('testclient', debug: true, httpClient: FakeMatrixApi()); + var client2 = Client('testclient', httpClient: FakeMatrixApi()); client2.database = client1.database; client2.connect(); await Future.delayed(Duration(milliseconds: 100)); expect(client2.isLogged(), true); - expect(client2.api.accessToken, client1.api.accessToken); + expect(client2.accessToken, client1.accessToken); expect(client2.userID, client1.userID); - expect(client2.api.homeserver, client1.api.homeserver); + expect(client2.homeserver, client1.homeserver); expect(client2.deviceID, client1.deviceID); expect(client2.deviceName, client1.deviceName); if (client2.encryptionEnabled) { diff --git a/test/device_keys_list_test.dart b/test/device_keys_list_test.dart index a98ff1b..4b870d3 100644 --- a/test/device_keys_list_test.dart +++ b/test/device_keys_list_test.dart @@ -19,6 +19,7 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; @@ -74,9 +75,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.error('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; diff --git a/test/encryption/cross_signing_test.dart b/test/encryption/cross_signing_test.dart index 4ec212b..fe22982 100644 --- a/test/encryption/cross_signing_test.dart +++ b/test/encryption/cross_signing_test.dart @@ -19,6 +19,7 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; @@ -33,9 +34,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; diff --git a/test/encryption/encrypt_decrypt_room_message_test.dart b/test/encryption/encrypt_decrypt_room_message_test.dart index 70d75fa..711a4a4 100644 --- a/test/encryption/encrypt_decrypt_room_message_test.dart +++ b/test/encryption/encrypt_decrypt_room_message_test.dart @@ -17,6 +17,7 @@ */ import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; @@ -30,9 +31,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; diff --git a/test/encryption/encrypt_decrypt_to_device_test.dart b/test/encryption/encrypt_decrypt_to_device_test.dart index 5636e8b..4fbde05 100644 --- a/test/encryption/encrypt_decrypt_to_device_test.dart +++ b/test/encryption/encrypt_decrypt_to_device_test.dart @@ -17,6 +17,7 @@ */ import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; @@ -35,15 +36,14 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; Client client; - var otherClient = - Client('othertestclient', debug: true, httpClient: FakeMatrixApi()); + var otherClient = Client('othertestclient', httpClient: FakeMatrixApi()); DeviceKeys device; Map payload; @@ -54,7 +54,7 @@ void main() { otherClient.connect( newToken: 'abc', newUserID: '@othertest:fakeServer.notExisting', - newHomeserver: otherClient.api.homeserver, + newHomeserver: otherClient.homeserver, newDeviceName: 'Text Matrix Client', newDeviceID: 'FOXDEVICE', newOlmAccount: otherPickledOlmAccount, diff --git a/test/encryption/key_manager_test.dart b/test/encryption/key_manager_test.dart index 5b3025f..bdd0304 100644 --- a/test/encryption/key_manager_test.dart +++ b/test/encryption/key_manager_test.dart @@ -17,6 +17,7 @@ */ import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; @@ -30,9 +31,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; diff --git a/test/encryption/key_request_test.dart b/test/encryption/key_request_test.dart index c7dbb9f..b780d2b 100644 --- a/test/encryption/key_request_test.dart +++ b/test/encryption/key_request_test.dart @@ -18,6 +18,7 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; @@ -45,9 +46,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; @@ -106,7 +107,7 @@ void main() { 'requesting_device_id': 'OTHERDEVICE', }); await matrix.encryption.keyManager.handleToDeviceEvent(event); - print(FakeMatrixApi.calledEndpoints.keys.toString()); + Logs.info(FakeMatrixApi.calledEndpoints.keys.toString()); expect( FakeMatrixApi.calledEndpoints.keys.any( (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), diff --git a/test/encryption/key_verification_test.dart b/test/encryption/key_verification_test.dart index 66ccdee..2612207 100644 --- a/test/encryption/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -20,6 +20,7 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/encryption.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; @@ -67,9 +68,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; @@ -82,14 +83,13 @@ void main() { test('setupClient', () async { client1 = await getClient(); - client2 = - Client('othertestclient', debug: true, httpClient: FakeMatrixApi()); + client2 = Client('othertestclient', httpClient: FakeMatrixApi()); client2.database = client1.database; await client2.checkServer('https://fakeServer.notExisting'); client2.connect( newToken: 'abc', newUserID: '@othertest:fakeServer.notExisting', - newHomeserver: client2.api.homeserver, + newHomeserver: client2.homeserver, newDeviceName: 'Text Matrix Client', newDeviceID: 'FOXDEVICE', newOlmAccount: otherPickledOlmAccount, diff --git a/test/encryption/olm_manager_test.dart b/test/encryption/olm_manager_test.dart index bf0e7a3..78e7068 100644 --- a/test/encryption/olm_manager_test.dart +++ b/test/encryption/olm_manager_test.dart @@ -18,6 +18,7 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; @@ -32,9 +33,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; diff --git a/test/encryption/online_key_backup_test.dart b/test/encryption/online_key_backup_test.dart index 0a3b842..12b9ae0 100644 --- a/test/encryption/online_key_backup_test.dart +++ b/test/encryption/online_key_backup_test.dart @@ -17,6 +17,7 @@ */ import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; @@ -30,9 +31,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; diff --git a/test/encryption/ssss_test.dart b/test/encryption/ssss_test.dart index a0d5b94..d213248 100644 --- a/test/encryption/ssss_test.dart +++ b/test/encryption/ssss_test.dart @@ -22,6 +22,7 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; import 'package:famedlysdk/encryption.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import 'package:test/test.dart'; import 'package:encrypt/encrypt.dart'; import 'package:olm/olm.dart' as olm; @@ -37,9 +38,9 @@ void main() { olm.Account(); } catch (_) { olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString()); } - print('[LibOlm] Enabled: $olmEnabled'); + Logs.success('[LibOlm] Enabled: $olmEnabled'); if (!olmEnabled) return; @@ -89,7 +90,7 @@ void main() { // account_data for this test final content = FakeMatrixApi .calledEndpoints[ - '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal'] + '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal'] .first; client.accountData['best animal'] = BasicEvent.fromJson({ 'type': 'best animal', diff --git a/test/event_test.dart b/test/event_test.dart index 035b55b..f40f8ec 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -50,7 +50,7 @@ void main() { 'status': 2, 'content': contentJson, }; - var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + var client = Client('testclient', httpClient: FakeMatrixApi()); var event = Event.fromJson( jsonObj, Room(id: '!localpart:server.abc', client: client)); @@ -67,7 +67,7 @@ void main() { expect(event.formattedText, formatted_body); expect(event.body, body); expect(event.type, EventTypes.Message); - expect(event.isReply, true); + expect(event.relationshipType, RelationshipTypes.Reply); jsonObj['state_key'] = ''; var state = Event.fromJson(jsonObj, null); expect(state.eventId, id); @@ -160,7 +160,43 @@ void main() { 'event_id': '1234', }; event = Event.fromJson(jsonObj, null); - expect(event.messageType, MessageTypes.Reply); + expect(event.messageType, MessageTypes.Text); + expect(event.relationshipType, RelationshipTypes.Reply); + expect(event.relationshipEventId, '1234'); + }); + + test('relationship types', () async { + Event event; + + jsonObj['content'] = { + 'msgtype': 'm.text', + 'text': 'beep', + }; + event = Event.fromJson(jsonObj, null); + expect(event.relationshipType, null); + expect(event.relationshipEventId, null); + + jsonObj['content']['m.relates_to'] = { + 'rel_type': 'm.replace', + 'event_id': 'abc', + }; + event = Event.fromJson(jsonObj, null); + expect(event.relationshipType, RelationshipTypes.Edit); + expect(event.relationshipEventId, 'abc'); + + jsonObj['content']['m.relates_to']['rel_type'] = 'm.annotation'; + event = Event.fromJson(jsonObj, null); + expect(event.relationshipType, RelationshipTypes.Reaction); + expect(event.relationshipEventId, 'abc'); + + jsonObj['content']['m.relates_to'] = { + 'm.in_reply_to': { + 'event_id': 'def', + }, + }; + event = Event.fromJson(jsonObj, null); + expect(event.relationshipType, RelationshipTypes.Reply); + expect(event.relationshipEventId, 'def'); }); test('redact', () async { @@ -175,8 +211,7 @@ void main() { ]; for (final testType in testTypes) { redactJsonObj['type'] = testType; - final room = - Room(id: '1234', client: Client('testclient', debug: true)); + final room = Room(id: '1234', client: Client('testclient')); final redactionEventJson = { 'content': {'reason': 'Spamming'}, 'event_id': '143273582443PhrSn:example.org', @@ -200,7 +235,7 @@ void main() { test('remove', () async { var event = Event.fromJson( - jsonObj, Room(id: '1234', client: Client('testclient', debug: true))); + jsonObj, Room(id: '1234', client: Client('testclient'))); final removed1 = await event.remove(); event.status = 0; final removed2 = await event.remove(); @@ -209,10 +244,9 @@ void main() { }); test('sendAgain', () async { - var matrix = - Client('testclient', debug: true, httpClient: FakeMatrixApi()); + var matrix = Client('testclient', httpClient: FakeMatrixApi()); await matrix.checkServer('https://fakeServer.notExisting'); - await matrix.login('test', '1234'); + await matrix.login(user: 'test', password: '1234'); var event = Event.fromJson( jsonObj, Room(id: '!1234:example.com', client: matrix)); @@ -226,10 +260,9 @@ void main() { }); test('requestKey', () async { - var matrix = - Client('testclient', debug: true, httpClient: FakeMatrixApi()); + var matrix = Client('testclient', httpClient: FakeMatrixApi()); await matrix.checkServer('https://fakeServer.notExisting'); - await matrix.login('test', '1234'); + await matrix.login(user: 'test', password: '1234'); var event = Event.fromJson( jsonObj, Room(id: '!1234:example.com', client: matrix)); @@ -274,8 +307,7 @@ void main() { expect(event.canRedact, true); }); test('getLocalizedBody', () async { - final matrix = - Client('testclient', debug: true, httpClient: FakeMatrixApi()); + final matrix = Client('testclient', httpClient: FakeMatrixApi()); final room = Room(id: '!1234:example.com', client: matrix); var event = Event.fromJson({ 'content': { @@ -790,5 +822,143 @@ void main() { }, room); expect(event.getLocalizedBody(FakeMatrixLocalizations()), null); }); + + test('aggregations', () { + var event = Event.fromJson({ + 'content': { + 'body': 'blah', + 'msgtype': 'm.text', + }, + 'event_id': '\$source', + }, null); + var edit1 = Event.fromJson({ + 'content': { + 'body': 'blah', + 'msgtype': 'm.text', + 'm.relates_to': { + 'event_id': '\$source', + 'rel_type': RelationshipTypes.Edit, + }, + }, + 'event_id': '\$edit1', + }, null); + var edit2 = Event.fromJson({ + 'content': { + 'body': 'blah', + 'msgtype': 'm.text', + 'm.relates_to': { + 'event_id': '\$source', + 'rel_type': RelationshipTypes.Edit, + }, + }, + 'event_id': '\$edit2', + }, null); + var room = Room(client: client); + var timeline = Timeline(events: [event, edit1, edit2], room: room); + expect(event.hasAggregatedEvents(timeline, RelationshipTypes.Edit), true); + expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit), + {edit1, edit2}); + expect(event.aggregatedEvents(timeline, RelationshipTypes.Reaction), + {}); + expect(event.hasAggregatedEvents(timeline, RelationshipTypes.Reaction), + false); + + timeline.removeAggregatedEvent(edit2); + expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit), {edit1}); + timeline.addAggregatedEvent(edit2); + expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit), + {edit1, edit2}); + + timeline.removeAggregatedEvent(event); + expect( + event.aggregatedEvents(timeline, RelationshipTypes.Edit), {}); + }); + test('getDisplayEvent', () { + var event = Event.fromJson({ + 'type': EventTypes.Message, + 'content': { + 'body': 'blah', + 'msgtype': 'm.text', + }, + 'event_id': '\$source', + 'sender': '@alice:example.org', + }, null); + event.sortOrder = 0; + var edit1 = Event.fromJson({ + 'type': EventTypes.Message, + 'content': { + 'body': '* edit 1', + 'msgtype': 'm.text', + 'm.new_content': { + 'body': 'edit 1', + 'msgtype': 'm.text', + }, + 'm.relates_to': { + 'event_id': '\$source', + 'rel_type': RelationshipTypes.Edit, + }, + }, + 'event_id': '\$edit1', + 'sender': '@alice:example.org', + }, null); + edit1.sortOrder = 1; + var edit2 = Event.fromJson({ + 'type': EventTypes.Message, + 'content': { + 'body': '* edit 2', + 'msgtype': 'm.text', + 'm.new_content': { + 'body': 'edit 2', + 'msgtype': 'm.text', + }, + 'm.relates_to': { + 'event_id': '\$source', + 'rel_type': RelationshipTypes.Edit, + }, + }, + 'event_id': '\$edit2', + 'sender': '@alice:example.org', + }, null); + edit2.sortOrder = 2; + var edit3 = Event.fromJson({ + 'type': EventTypes.Message, + 'content': { + 'body': '* edit 3', + 'msgtype': 'm.text', + 'm.new_content': { + 'body': 'edit 3', + 'msgtype': 'm.text', + }, + 'm.relates_to': { + 'event_id': '\$source', + 'rel_type': RelationshipTypes.Edit, + }, + }, + 'event_id': '\$edit3', + 'sender': '@bob:example.org', + }, null); + edit3.sortOrder = 3; + var room = Room(client: client); + // no edits + var displayEvent = + event.getDisplayEvent(Timeline(events: [event], room: room)); + expect(displayEvent.body, 'blah'); + // one edit + displayEvent = event + .getDisplayEvent(Timeline(events: [event, edit1], room: room)); + expect(displayEvent.body, 'edit 1'); + // two edits + displayEvent = event.getDisplayEvent( + Timeline(events: [event, edit1, edit2], room: room)); + expect(displayEvent.body, 'edit 2'); + // foreign edit + displayEvent = event + .getDisplayEvent(Timeline(events: [event, edit3], room: room)); + expect(displayEvent.body, 'blah'); + // mixed foreign and non-foreign + displayEvent = event.getDisplayEvent( + Timeline(events: [event, edit1, edit2, edit3], room: room)); + expect(displayEvent.body, 'edit 2'); + }); }); } diff --git a/test/fake_client.dart b/test/fake_client.dart index af2c39a..5f30487 100644 --- a/test/fake_client.dart +++ b/test/fake_client.dart @@ -29,21 +29,15 @@ const pickledOlmAccount = 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw'; Future getClient() async { - final client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + final client = Client('testclient', httpClient: FakeMatrixApi()); client.database = getDatabase(); await client.checkServer('https://fakeServer.notExisting'); - final resp = await client.api.login( - type: 'm.login.password', - user: 'test', - password: '1234', - initialDeviceDisplayName: 'Fluffy Matrix Client', - ); client.connect( - newToken: resp.accessToken, - newUserID: resp.userId, - newHomeserver: client.api.homeserver, + newToken: 'abcd', + newUserID: '@test:fakeServer.notExisting', + newHomeserver: client.homeserver, newDeviceName: 'Text Matrix Client', - newDeviceID: resp.deviceId, + newDeviceID: 'GHTYAJCE', newOlmAccount: pickledOlmAccount, ); await Future.delayed(Duration(milliseconds: 10)); diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 2a149d4..2dec135 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -80,8 +80,12 @@ class FakeMatrixApi extends MockClient { res = {'displayname': ''}; } else if (method == 'PUT' && action.contains( - '/client/r0/rooms/%211234%3AfakeServer.notExisting/send/')) { + '/client/r0/rooms/!1234%3AfakeServer.notExisting/send/')) { res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'}; + } else if (action.contains('/client/r0/sync')) { + res = { + 'next_batch': DateTime.now().millisecondsSinceEpoch.toString + }; } else { res = { 'errcode': 'M_UNRECOGNIZED', @@ -748,7 +752,7 @@ class FakeMatrixApi extends MockClient { 'app_url': 'https://custom.app.example.org' } }, - '/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags': + '/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags': (var req) => { 'tags': { 'm.favourite': {'order': 0.1}, @@ -1982,21 +1986,21 @@ class FakeMatrixApi extends MockClient { (var req) => {}, '/client/r0/pushrules/global/content/nocake/enabled': (var req) => {}, '/client/r0/pushrules/global/content/nocake/actions': (var req) => {}, - '/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.history_visibility': + '/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.history_visibility': (var req) => {}, - '/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.join_rules': + '/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.join_rules': (var req) => {}, - '/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.guest_access': + '/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.guest_access': (var req) => {}, - '/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.invite/1234': + '/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.call.invite/1234': (var req) => {}, - '/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.answer/1234': + '/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.call.answer/1234': (var req) => {}, - '/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.candidates/1234': + '/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.call.candidates/1234': (var req) => {}, - '/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.hangup/1234': + '/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.call.hangup/1234': (var req) => {}, - '/client/r0/rooms/%211234%3Aexample.com/redact/1143273582443PhrSn%3Aexample.org/1234': + '/client/r0/rooms/!1234%3Aexample.com/redact/1143273582443PhrSn%3Aexample.org/1234': (var req) => {'event_id': '1234'}, '/client/r0/pushrules/global/room/!localpart%3Aserver.abc': (var req) => {}, @@ -2006,23 +2010,31 @@ class FakeMatrixApi extends MockClient { (var req) => {}, '/client/r0/devices/QBUAZIFURK': (var req) => {}, '/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {}, - '/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/testtxid': + '/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.message/testtxid': + (var reqI) => { + 'event_id': '\$event${FakeMatrixApi.eventCounter++}', + }, + '/client/r0/rooms/!localpart%3Aserver.abc/send/m.reaction/testtxid': (var reqI) => { 'event_id': '\$event${FakeMatrixApi.eventCounter++}', }, '/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com': (var req) => {}, - '/client/r0/rooms/%211234%3Aexample.com/send/m.room.message/1234': + '/client/r0/rooms/!1234%3Aexample.com/send/m.room.message/1234': (var reqI) => { 'event_id': '\$event${FakeMatrixApi.eventCounter++}', }, - '/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%21localpart%3Aserver.abc/tags/m.favourite': + '/client/r0/rooms/!1234%3Aexample.com/send/m.room.message/newresend': + (var reqI) => { + 'event_id': '\$event${FakeMatrixApi.eventCounter++}', + }, + '/client/r0/user/%40test%3AfakeServer.notExisting/rooms/!localpart%3Aserver.abc/tags/m.favourite': (var req) => {}, - '/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag': + '/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags/testtag': (var req) => {}, '/client/r0/user/%40alice%3Aexample.com/account_data/test.account.data': (var req) => {}, - '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal': + '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal': (var req) => {}, '/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data': (var req) => {}, @@ -2034,27 +2046,27 @@ class FakeMatrixApi extends MockClient { '/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {}, '/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url': (var reqI) => {}, - '/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.encryption': + '/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.encryption': (var reqI) => {'event_id': 'YUwRidLecu:example.com'}, - '/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.avatar': + '/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.avatar': (var reqI) => {'event_id': 'YUwRidLecu:example.com'}, - '/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/1234': + '/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.message/1234': (var reqI) => {'event_id': 'YUwRidLecu:example.com'}, - '/client/r0/rooms/%21localpart%3Aserver.abc/redact/1234/1234': - (var reqI) => {'event_id': 'YUwRidLecu:example.com'}, - '/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.name': + '/client/r0/rooms/!localpart%3Aserver.abc/redact/1234/1234': (var reqI) => + {'event_id': 'YUwRidLecu:example.com'}, + '/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.name': (var reqI) => { 'event_id': '42', }, - '/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.topic': + '/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.topic': (var reqI) => { 'event_id': '42', }, - '/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.pinned_events': + '/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.pinned_events': (var reqI) => { 'event_id': '42', }, - '/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.power_levels': + '/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.power_levels': (var reqI) => { 'event_id': '42', }, @@ -2083,9 +2095,9 @@ class FakeMatrixApi extends MockClient { '/client/r0/pushrules/global/content/nocake': (var req) => {}, '/client/r0/pushrules/global/override/!localpart%3Aserver.abc': (var req) => {}, - '/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%21localpart%3Aserver.abc/tags/m.favourite': + '/client/r0/user/%40test%3AfakeServer.notExisting/rooms/!localpart%3Aserver.abc/tags/m.favourite': (var req) => {}, - '/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag': + '/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags/testtag': (var req) => {}, '/client/unstable/room_keys/version/5': (var req) => {}, '/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5': diff --git a/test/markdown_test.dart b/test/markdown_test.dart index ff7585d..9a4b999 100644 --- a/test/markdown_test.dart +++ b/test/markdown_test.dart @@ -54,11 +54,11 @@ void main() { }); test('emotes', () { expect(markdown(':fox:', emotePacks), - ':fox:'); + ':fox:'); expect(markdown(':user~fox:', emotePacks), - ':fox:'); + ':fox:'); expect(markdown(':raccoon:', emotePacks), - ':raccoon:'); + ':raccoon:'); expect(markdown(':invalid:', emotePacks), ':invalid:'); expect(markdown(':room~invalid:', emotePacks), ':room~invalid:'); }); diff --git a/test/matrix_api_test.dart b/test/matrix_api_test.dart index a165628..5336cce 100644 --- a/test/matrix_api_test.dart +++ b/test/matrix_api_test.dart @@ -33,7 +33,6 @@ void main() { group('Matrix API', () { final matrixApi = MatrixApi( httpClient: FakeMatrixApi(), - debug: true, ); test('MatrixException test', () async { final exception = MatrixException.fromJson({ @@ -1377,7 +1376,7 @@ void main() { '@alice:example.com', '!localpart:example.com'); expect( FakeMatrixApi.api['GET'][ - '/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags']({}), + '/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags']({}), {'tags': response.map((k, v) => MapEntry(k, v.toJson()))}, ); diff --git a/test/matrix_database_test.dart b/test/matrix_database_test.dart new file mode 100644 index 0000000..0f16b63 --- /dev/null +++ b/test/matrix_database_test.dart @@ -0,0 +1,184 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:test/test.dart'; +import 'fake_database.dart'; + +void main() { + group('Databse', () { + final database = getDatabase(); + var clientId = -1; + var room = Room(id: '!room:blubb'); + test('setupDatabase', () async { + clientId = await database.insertClient( + 'testclient', + 'https://example.org', + 'blubb', + '@test:example.org', + null, + null, + null, + null); + }); + test('storeEventUpdate', () async { + // store a simple update + var update = EventUpdate( + type: 'timeline', + roomID: room.id, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'origin_server_ts': 100, + 'content': {'blah': 'blubb'}, + 'event_id': '\$event-1', + 'sender': '@blah:blubb', + }, + sortOrder: 0.0, + ); + await database.storeEventUpdate(clientId, update); + var event = await database.getEventById(clientId, '\$event-1', room); + expect(event.eventId, '\$event-1'); + + // insert a transaction id + update = EventUpdate( + type: 'timeline', + roomID: room.id, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'origin_server_ts': 100, + 'content': {'blah': 'blubb'}, + 'event_id': 'transaction-1', + 'sender': '@blah:blubb', + 'status': 0, + }, + sortOrder: 0.0, + ); + await database.storeEventUpdate(clientId, update); + event = await database.getEventById(clientId, 'transaction-1', room); + expect(event.eventId, 'transaction-1'); + update = EventUpdate( + type: 'timeline', + roomID: room.id, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'origin_server_ts': 100, + 'content': {'blah': 'blubb'}, + 'event_id': '\$event-2', + 'sender': '@blah:blubb', + 'unsigned': { + 'transaction_id': 'transaction-1', + }, + 'status': 1, + }, + sortOrder: 0.0, + ); + await database.storeEventUpdate(clientId, update); + event = await database.getEventById(clientId, 'transaction-1', room); + expect(event, null); + event = await database.getEventById(clientId, '\$event-2', room); + + // insert a transaction id if the event id for it already exists + update = EventUpdate( + type: 'timeline', + roomID: room.id, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'origin_server_ts': 100, + 'content': {'blah': 'blubb'}, + 'event_id': '\$event-3', + 'sender': '@blah:blubb', + 'status': 0, + }, + sortOrder: 0.0, + ); + await database.storeEventUpdate(clientId, update); + event = await database.getEventById(clientId, '\$event-3', room); + expect(event.eventId, '\$event-3'); + update = EventUpdate( + type: 'timeline', + roomID: room.id, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'origin_server_ts': 100, + 'content': {'blah': 'blubb'}, + 'event_id': '\$event-3', + 'sender': '@blah:blubb', + 'status': 1, + 'unsigned': { + 'transaction_id': 'transaction-2', + }, + }, + sortOrder: 0.0, + ); + await database.storeEventUpdate(clientId, update); + event = await database.getEventById(clientId, '\$event-3', room); + expect(event.eventId, '\$event-3'); + expect(event.status, 1); + event = await database.getEventById(clientId, 'transaction-2', room); + expect(event, null); + + // insert transaction id and not update status + update = EventUpdate( + type: 'timeline', + roomID: room.id, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'origin_server_ts': 100, + 'content': {'blah': 'blubb'}, + 'event_id': '\$event-4', + 'sender': '@blah:blubb', + 'status': 2, + }, + sortOrder: 0.0, + ); + await database.storeEventUpdate(clientId, update); + event = await database.getEventById(clientId, '\$event-4', room); + expect(event.eventId, '\$event-4'); + update = EventUpdate( + type: 'timeline', + roomID: room.id, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'origin_server_ts': 100, + 'content': {'blah': 'blubb'}, + 'event_id': '\$event-4', + 'sender': '@blah:blubb', + 'status': 1, + 'unsigned': { + 'transaction_id': 'transaction-3', + }, + }, + sortOrder: 0.0, + ); + await database.storeEventUpdate(clientId, update); + event = await database.getEventById(clientId, '\$event-4', room); + expect(event.eventId, '\$event-4'); + expect(event.status, 2); + event = await database.getEventById(clientId, 'transaction-3', room); + expect(event, null); + }); + }); +} diff --git a/test/mxc_uri_extension_test.dart b/test/mxc_uri_extension_test.dart index 798854c..30e6f84 100644 --- a/test/mxc_uri_extension_test.dart +++ b/test/mxc_uri_extension_test.dart @@ -33,13 +33,13 @@ void main() { expect(content.isScheme('mxc'), true); expect(content.getDownloadLink(client), - '${client.api.homeserver.toString()}/_matrix/media/r0/download/exampleserver.abc/abcdefghijklmn'); + '${client.homeserver.toString()}/_matrix/media/r0/download/exampleserver.abc/abcdefghijklmn'); expect(content.getThumbnail(client, width: 50, height: 50), - '${client.api.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop'); + '${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop'); expect( content.getThumbnail(client, width: 50, height: 50, method: ThumbnailMethod.scale), - '${client.api.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale'); + '${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale'); }); }); } diff --git a/test/room_test.dart b/test/room_test.dart index b459db5..5bad302 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -27,7 +27,9 @@ import 'package:famedlysdk/src/database/database.dart' import 'package:test/test.dart'; import 'fake_client.dart'; +import 'fake_matrix_api.dart'; +import 'dart:convert'; import 'dart:typed_data'; void main() { @@ -349,9 +351,87 @@ void main() { }); test('sendEvent', () async { + FakeMatrixApi.calledEndpoints.clear(); final dynamic resp = await room.sendTextEvent('Hello world', txid: 'testtxid'); expect(resp.startsWith('\$event'), true); + final entry = FakeMatrixApi.calledEndpoints.entries + .firstWhere((p) => p.key.contains('/send/m.room.message/')); + final content = json.decode(entry.value.first); + expect(content, { + 'body': 'Hello world', + 'msgtype': 'm.text', + }); + }); + + test('send edit', () async { + FakeMatrixApi.calledEndpoints.clear(); + final dynamic resp = await room.sendTextEvent('Hello world', + txid: 'testtxid', editEventId: '\$otherEvent'); + expect(resp.startsWith('\$event'), true); + final entry = FakeMatrixApi.calledEndpoints.entries + .firstWhere((p) => p.key.contains('/send/m.room.message/')); + final content = json.decode(entry.value.first); + expect(content, { + 'body': '* Hello world', + 'msgtype': 'm.text', + 'm.new_content': { + 'body': 'Hello world', + 'msgtype': 'm.text', + }, + 'm.relates_to': { + 'event_id': '\$otherEvent', + 'rel_type': 'm.replace', + }, + }); + }); + + test('send reply', () async { + var event = Event.fromJson({ + 'event_id': '\$replyEvent', + 'content': { + 'body': 'Blah', + 'msgtype': 'm.text', + }, + 'type': 'm.room.message', + 'sender': '@alice:example.org', + }, room); + FakeMatrixApi.calledEndpoints.clear(); + final dynamic resp = await room.sendTextEvent('Hello world', + txid: 'testtxid', inReplyTo: event); + expect(resp.startsWith('\$event'), true); + final entry = FakeMatrixApi.calledEndpoints.entries + .firstWhere((p) => p.key.contains('/send/m.room.message/')); + final content = json.decode(entry.value.first); + expect(content, { + 'body': '> <@alice:example.org> Blah\n\nHello world', + 'msgtype': 'm.text', + 'format': 'org.matrix.custom.html', + 'formatted_body': + '
In reply to @alice:example.org
Blah
Hello world', + 'm.relates_to': { + 'm.in_reply_to': { + 'event_id': '\$replyEvent', + }, + }, + }); + }); + + test('send reaction', () async { + FakeMatrixApi.calledEndpoints.clear(); + final dynamic resp = + await room.sendReaction('\$otherEvent', '🦊', txid: 'testtxid'); + expect(resp.startsWith('\$event'), true); + final entry = FakeMatrixApi.calledEndpoints.entries + .firstWhere((p) => p.key.contains('/send/m.reaction/')); + final content = json.decode(entry.value.first); + expect(content, { + 'm.relates_to': { + 'event_id': '\$otherEvent', + 'rel_type': 'm.annotation', + 'key': '🦊', + }, + }); }); // Not working because there is no real file to test it... diff --git a/test/sync_filter_test.dart b/test/sync_filter_test.dart new file mode 100644 index 0000000..71ac01b --- /dev/null +++ b/test/sync_filter_test.dart @@ -0,0 +1,166 @@ +/* + * Ansible inventory script used at Famedly GmbH for managing many hosts + * Copyright (C) 2020 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:test/test.dart'; + +const UPDATES = { + 'empty': { + 'next_batch': 'blah', + 'account_data': { + 'events': [], + }, + 'presences': { + 'events': [], + }, + 'rooms': { + 'join': {}, + 'leave': {}, + 'invite': {}, + }, + 'to_device': { + 'events': [], + }, + }, + 'presence': { + 'next_batch': 'blah', + 'presence': { + 'events': [ + { + 'content': { + 'avatar_url': 'mxc://localhost:wefuiwegh8742w', + 'last_active_ago': 2478593, + 'presence': 'online', + 'currently_active': false, + 'status_msg': 'Making cupcakes' + }, + 'type': 'm.presence', + 'sender': '@example:localhost', + }, + ], + }, + }, + 'account_data': { + 'next_batch': 'blah', + 'account_data': { + 'events': [ + { + 'type': 'blah', + 'content': { + 'beep': 'boop', + }, + }, + ], + }, + }, + 'invite': { + 'next_batch': 'blah', + 'rooms': { + 'invite': { + '!room': { + 'invite_state': { + 'events': [], + }, + }, + }, + }, + }, + 'leave': { + 'next_batch': 'blah', + 'rooms': { + 'leave': { + '!room': {}, + }, + }, + }, + 'join': { + 'next_batch': 'blah', + 'rooms': { + 'join': { + '!room': { + 'timeline': { + 'events': [], + }, + 'state': { + 'events': [], + }, + 'account_data': { + 'events': [], + }, + 'ephemeral': { + 'events': [], + }, + 'unread_notifications': {}, + 'summary': {}, + }, + }, + }, + }, + 'to_device': { + 'next_batch': 'blah', + 'to_device': { + 'events': [ + { + 'type': 'beep', + 'content': { + 'blah': 'blubb', + }, + }, + ], + }, + }, +}; + +void testUpdates(bool Function(SyncUpdate s) test, Map expected) { + for (final update in UPDATES.entries) { + var sync = SyncUpdate.fromJson(update.value); + expect(test(sync), expected[update.key]); + } +} + +void main() { + group('Sync Filters', () { + test('room update', () { + var testFn = (SyncUpdate s) => s.hasRoomUpdate; + final expected = { + 'empty': false, + 'presence': false, + 'account_data': true, + 'invite': true, + 'leave': true, + 'join': true, + 'to_device': true, + }; + testUpdates(testFn, expected); + }); + + test('presence update', () { + var testFn = (SyncUpdate s) => s.hasPresenceUpdate; + final expected = { + 'empty': false, + 'presence': true, + 'account_data': false, + 'invite': false, + 'leave': false, + 'join': false, + 'to_device': false, + }; + testUpdates(testFn, expected); + }); + }); +} diff --git a/test/timeline_test.dart b/test/timeline_test.dart index 005e8b0..0725d19 100644 --- a/test/timeline_test.dart +++ b/test/timeline_test.dart @@ -33,7 +33,7 @@ void main() { var updateCount = 0; var insertList = []; - var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + var client = Client('testclient', httpClient: FakeMatrixApi()); var room = Room( id: roomID, client: client, prev_batch: '1234', roomAccountData: {}); @@ -186,8 +186,12 @@ void main() { }, sortOrder: room.newSortOrder)); await Future.delayed(Duration(milliseconds: 50)); + + expect(updateCount, 7); await room.sendTextEvent('test', txid: 'errortxid'); await Future.delayed(Duration(milliseconds: 50)); + + expect(updateCount, 9); await room.sendTextEvent('test', txid: 'errortxid2'); await Future.delayed(Duration(milliseconds: 50)); await room.sendTextEvent('test', txid: 'errortxid3'); @@ -214,14 +218,29 @@ void main() { }); test('Resend message', () async { - await timeline.events[0].sendAgain(txid: '1234'); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': -1, + 'event_id': 'new-test-event', + 'origin_server_ts': testTimeStamp, + 'unsigned': {'transaction_id': 'newresend'}, + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + await timeline.events[0].sendAgain(); await Future.delayed(Duration(milliseconds: 50)); expect(updateCount, 17); expect(insertList, [0, 0, 0, 0, 0, 0, 0, 0]); - expect(timeline.events.length, 6); + expect(timeline.events.length, 7); expect(timeline.events[0].status, 1); }); @@ -231,12 +250,12 @@ void main() { await Future.delayed(Duration(milliseconds: 50)); expect(updateCount, 20); - expect(timeline.events.length, 9); - expect(timeline.events[6].eventId, '3143273582443PhrSn:example.org'); - expect(timeline.events[7].eventId, '2143273582443PhrSn:example.org'); - expect(timeline.events[8].eventId, '1143273582443PhrSn:example.org'); + expect(timeline.events.length, 10); + expect(timeline.events[7].eventId, '3143273582443PhrSn:example.org'); + expect(timeline.events[8].eventId, '2143273582443PhrSn:example.org'); + expect(timeline.events[9].eventId, '1143273582443PhrSn:example.org'); expect(room.prev_batch, 't47409-4357353_219380_26003_2265'); - await timeline.events[8].redact(reason: 'test', txid: '1234'); + await timeline.events[9].redact(reason: 'test', txid: '1234'); }); test('Clear cache on limited timeline', () async { @@ -251,5 +270,253 @@ void main() { await Future.delayed(Duration(milliseconds: 50)); expect(timeline.events.isEmpty, true); }); + + test('sending event to failed update', () async { + timeline.events.clear(); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 0, + 'event_id': 'will-fail', + 'origin_server_ts': testTimeStamp + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 0); + expect(timeline.events.length, 1); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': -1, + 'event_id': 'will-fail', + 'origin_server_ts': testTimeStamp + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, -1); + expect(timeline.events.length, 1); + }); + test('sending an event and the http request finishes first, 0 -> 1 -> 2', + () async { + timeline.events.clear(); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 0, + 'event_id': 'transaction', + 'origin_server_ts': testTimeStamp + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 0); + expect(timeline.events.length, 1); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 1, + 'event_id': '\$event', + 'origin_server_ts': testTimeStamp, + 'unsigned': {'transaction_id': 'transaction'} + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 1); + expect(timeline.events.length, 1); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 2, + 'event_id': '\$event', + 'origin_server_ts': testTimeStamp, + 'unsigned': {'transaction_id': 'transaction'} + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 2); + expect(timeline.events.length, 1); + }); + test('sending an event where the sync reply arrives first, 0 -> 2 -> 1', + () async { + timeline.events.clear(); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 0, + 'event_id': 'transaction', + 'origin_server_ts': testTimeStamp + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 0); + expect(timeline.events.length, 1); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 2, + 'event_id': '\$event', + 'origin_server_ts': testTimeStamp, + 'unsigned': {'transaction_id': 'transaction'} + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 2); + expect(timeline.events.length, 1); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 1, + 'event_id': '\$event', + 'origin_server_ts': testTimeStamp, + 'unsigned': {'transaction_id': 'transaction'} + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 2); + expect(timeline.events.length, 1); + }); + test('sending an event 0 -> -1 -> 2', () async { + timeline.events.clear(); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 0, + 'event_id': 'transaction', + 'origin_server_ts': testTimeStamp + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 0); + expect(timeline.events.length, 1); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': -1, + 'origin_server_ts': testTimeStamp, + 'unsigned': {'transaction_id': 'transaction'}, + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, -1); + expect(timeline.events.length, 1); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 2, + 'event_id': '\$event', + 'origin_server_ts': testTimeStamp, + 'unsigned': {'transaction_id': 'transaction'}, + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 2); + expect(timeline.events.length, 1); + }); + test('sending an event 0 -> 2 -> -1', () async { + timeline.events.clear(); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 0, + 'event_id': 'transaction', + 'origin_server_ts': testTimeStamp + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 0); + expect(timeline.events.length, 1); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': 2, + 'event_id': '\$event', + 'origin_server_ts': testTimeStamp, + 'unsigned': {'transaction_id': 'transaction'}, + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 2); + expect(timeline.events.length, 1); + client.onEvent.add(EventUpdate( + type: 'timeline', + roomID: roomID, + eventType: 'm.room.message', + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': -1, + 'origin_server_ts': testTimeStamp, + 'unsigned': {'transaction_id': 'transaction'}, + }, + sortOrder: room.newSortOrder)); + await Future.delayed(Duration(milliseconds: 50)); + expect(timeline.events[0].status, 2); + expect(timeline.events.length, 1); + }); }); } diff --git a/test/user_test.dart b/test/user_test.dart index 05e415b..0e7de24 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -27,7 +27,7 @@ import 'fake_matrix_api.dart'; void main() { /// All Tests related to the Event group('User', () { - var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + var client = Client('testclient', httpClient: FakeMatrixApi()); final user1 = User( '@alice:example.com', membership: 'join', @@ -102,7 +102,7 @@ void main() { }); test('startDirectChat', () async { await client.checkServer('https://fakeserver.notexisting'); - await client.login('test', '1234'); + await client.login(user: 'test', password: '1234'); await user1.startDirectChat(); }); test('getPresence', () async { diff --git a/test_driver/famedlysdk_test.dart b/test_driver/famedlysdk_test.dart index 79cabfb..f2f3988 100644 --- a/test_driver/famedlysdk_test.dart +++ b/test_driver/famedlysdk_test.dart @@ -1,5 +1,6 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; +import 'package:famedlysdk/src/utils/logs.dart'; import '../test/fake_database.dart'; void main() => test(); @@ -17,21 +18,21 @@ const String testMessage5 = 'Hello earth'; const String testMessage6 = 'Hello mars'; void test() async { - print('++++ Login $testUserA ++++'); - var testClientA = Client('TestClientA', debug: false); + Logs.success('++++ Login $testUserA ++++'); + var testClientA = Client('TestClientA'); testClientA.database = getDatabase(); await testClientA.checkServer(homeserver); - await testClientA.login(testUserA, testPasswordA); + await testClientA.login(user: testUserA, password: testPasswordA); assert(testClientA.encryptionEnabled); - print('++++ Login $testUserB ++++'); - var testClientB = Client('TestClientB', debug: false); + Logs.success('++++ Login $testUserB ++++'); + var testClientB = Client('TestClientB'); testClientB.database = getDatabase(); await testClientB.checkServer(homeserver); - await testClientB.login(testUserB, testPasswordA); + await testClientB.login(user: testUserB, password: testPasswordA); assert(testClientB.encryptionEnabled); - print('++++ ($testUserA) Leave all rooms ++++'); + Logs.success('++++ ($testUserA) Leave all rooms ++++'); while (testClientA.rooms.isNotEmpty) { var room = testClientA.rooms.first; if (room.canonicalAlias?.isNotEmpty ?? false) { @@ -43,7 +44,7 @@ void test() async { } catch (_) {} } - print('++++ ($testUserB) Leave all rooms ++++'); + Logs.success('++++ ($testUserB) Leave all rooms ++++'); for (var i = 0; i < 3; i++) { if (testClientB.rooms.isNotEmpty) { var room = testClientB.rooms.first; @@ -54,7 +55,7 @@ void test() async { } } - print('++++ Check if own olm device is verified by default ++++'); + Logs.success('++++ Check if own olm device is verified by default ++++'); assert(testClientA.userDeviceKeys.containsKey(testUserA)); assert(testClientA.userDeviceKeys[testUserA].deviceKeys .containsKey(testClientA.deviceID)); @@ -70,20 +71,20 @@ void test() async { assert(!testClientB .userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].blocked); - print('++++ ($testUserA) Create room and invite $testUserB ++++'); - await testClientA.api.createRoom(invite: [testUserB]); + Logs.success('++++ ($testUserA) Create room and invite $testUserB ++++'); + await testClientA.createRoom(invite: [testUserB]); await Future.delayed(Duration(seconds: 1)); var room = testClientA.rooms.first; assert(room != null); final roomId = room.id; - print('++++ ($testUserB) Join room ++++'); + Logs.success('++++ ($testUserB) Join room ++++'); var inviteRoom = testClientB.getRoomById(roomId); await inviteRoom.join(); await Future.delayed(Duration(seconds: 1)); assert(inviteRoom.membership == Membership.join); - print('++++ ($testUserA) Enable encryption ++++'); + Logs.success('++++ ($testUserA) Enable encryption ++++'); assert(room.encrypted == false); await room.enableEncryption(); await Future.delayed(Duration(seconds: 5)); @@ -91,7 +92,7 @@ void test() async { assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) == null); - print('++++ ($testUserA) Check known olm devices ++++'); + Logs.success('++++ ($testUserA) Check known olm devices ++++'); assert(testClientA.userDeviceKeys.containsKey(testUserB)); assert(testClientA.userDeviceKeys[testUserB].deviceKeys .containsKey(testClientB.deviceID)); @@ -109,7 +110,7 @@ void test() async { await testClientA.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID] .setVerified(true); - print('++++ Check if own olm device is verified by default ++++'); + Logs.success('++++ Check if own olm device is verified by default ++++'); assert(testClientA.userDeviceKeys.containsKey(testUserA)); assert(testClientA.userDeviceKeys[testUserA].deviceKeys .containsKey(testClientA.deviceID)); @@ -121,7 +122,7 @@ void test() async { assert(testClientB .userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].verified); - print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++"); + Logs.success("++++ ($testUserA) Send encrypted message: '$testMessage' ++++"); await room.sendTextEvent(testMessage); await Future.delayed(Duration(seconds: 5)); assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) != @@ -148,10 +149,11 @@ void test() async { null); assert(room.lastMessage == testMessage); assert(inviteRoom.lastMessage == testMessage); - print( + Logs.success( "++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++"); - print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++"); + Logs.success( + "++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++"); await room.sendTextEvent(testMessage2); await Future.delayed(Duration(seconds: 5)); assert(testClientA @@ -175,10 +177,11 @@ void test() async { null); assert(room.lastMessage == testMessage2); assert(inviteRoom.lastMessage == testMessage2); - print( + Logs.success( "++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++"); - print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++"); + Logs.success( + "++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++"); await inviteRoom.sendTextEvent(testMessage3); await Future.delayed(Duration(seconds: 5)); assert(testClientA @@ -208,17 +211,17 @@ void test() async { null); assert(inviteRoom.lastMessage == testMessage3); assert(room.lastMessage == testMessage3); - print( + Logs.success( "++++ ($testUserA) Received decrypted message: '${room.lastMessage}' ++++"); - print('++++ Login $testUserB in another client ++++'); - var testClientC = - Client('TestClientC', debug: false, database: getDatabase()); + Logs.success('++++ Login $testUserB in another client ++++'); + var testClientC = Client('TestClientC', database: getDatabase()); await testClientC.checkServer(homeserver); - await testClientC.login(testUserB, testPasswordA); + await testClientC.login(user: testUserB, password: testPasswordA); await Future.delayed(Duration(seconds: 3)); - print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++"); + Logs.success( + "++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++"); await room.sendTextEvent(testMessage4); await Future.delayed(Duration(seconds: 5)); assert(testClientA @@ -255,16 +258,17 @@ void test() async { null); assert(room.lastMessage == testMessage4); assert(inviteRoom.lastMessage == testMessage4); - print( + Logs.success( "++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++"); - print('++++ Logout $testUserB another client ++++'); + Logs.success('++++ Logout $testUserB another client ++++'); await testClientC.dispose(); await testClientC.logout(); testClientC = null; await Future.delayed(Duration(seconds: 5)); - print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++"); + Logs.success( + "++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++"); await room.sendTextEvent(testMessage6); await Future.delayed(Duration(seconds: 5)); assert(testClientA @@ -291,10 +295,10 @@ void test() async { null); assert(room.lastMessage == testMessage6); assert(inviteRoom.lastMessage == testMessage6); - print( + Logs.success( "++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++"); -/* print('++++ ($testUserA) Restore user ++++'); +/* Logs.success('++++ ($testUserA) Restore user ++++'); await testClientA.dispose(); testClientA = null; testClientA = Client( @@ -321,7 +325,7 @@ void test() async { assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() == testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id()); - print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++"); + Logs.success("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++"); await restoredRoom.sendTextEvent(testMessage5); await Future.delayed(Duration(seconds: 5)); assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1); @@ -331,10 +335,10 @@ void test() async { assert(restoredRoom.lastMessage == testMessage5); assert(inviteRoom.lastMessage == testMessage5); assert(testClientB.getRoomById(roomId).lastMessage == testMessage5); - print( + Logs.success( "++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");*/ - print('++++ Logout $testUserA and $testUserB ++++'); + Logs.success('++++ Logout $testUserA and $testUserB ++++'); await room.leave(); await room.forget(); await inviteRoom.leave(); @@ -342,8 +346,8 @@ void test() async { await Future.delayed(Duration(seconds: 1)); await testClientA.dispose(); await testClientB.dispose(); - await testClientA.api.logoutAll(); - await testClientB.api.logoutAll(); + await testClientA.logoutAll(); + await testClientB.logoutAll(); testClientA = null; testClientB = null; return;