From c68487ac2180ccddb9aeeb18e53c384122a823e4 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 23 Jul 2020 08:09:00 +0000 Subject: [PATCH] fix issue with sending messages --- lib/matrix_api/matrix_api.dart | 26 +-- lib/src/database/database.dart | 29 +++- lib/src/database/database.g.dart | 15 ++ lib/src/database/database.moor | 1 + lib/src/event.dart | 3 +- lib/src/timeline.dart | 74 +++++---- test/encryption/ssss_test.dart | 2 +- test/fake_matrix_api.dart | 56 ++++--- test/matrix_api_test.dart | 2 +- test/matrix_database_test.dart | 184 ++++++++++++++++++++ test/timeline_test.dart | 277 ++++++++++++++++++++++++++++++- 11 files changed, 587 insertions(+), 82 deletions(-) create mode 100644 test/matrix_database_test.dart diff --git a/lib/matrix_api/matrix_api.dart b/lib/matrix_api/matrix_api.dart index 770dd25..569f161 100644 --- a/lib/matrix_api/matrix_api.dart +++ b/lib/matrix_api/matrix_api.dart @@ -787,7 +787,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 +803,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 +818,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, }); @@ -1734,7 +1734,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 +1750,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 +1762,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 +1777,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 +1791,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 +1806,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 +1821,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 +1830,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 +1845,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 +1862,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/src/database/database.dart b/lib/src/database/database.dart index d339336..adb9fc6 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -361,9 +361,32 @@ class Database extends _$Database { 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..1e55b22 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -312,7 +312,8 @@ 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(); + // we do not remove the event here. It will automatically be updated + // in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2 final eventID = await room.sendEvent( content, txid: txid ?? unsigned['transaction_id'], diff --git a/lib/src/timeline.dart b/lib/src/timeline.dart index e219733..967b830 100644 --- a/lib/src/timeline.dart +++ b/lib/src/timeline.dart @@ -122,10 +122,31 @@ 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; } @@ -135,6 +156,7 @@ class Timeline { 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']); @@ -142,13 +164,10 @@ class Timeline { 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) { + } else { var i = _findEvent( event_id: eventUpdate.content['event_id'], unsigned_txid: eventUpdate.content['unsigned'] is Map @@ -156,34 +175,29 @@ 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; + } + } 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); + 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(); diff --git a/test/encryption/ssss_test.dart b/test/encryption/ssss_test.dart index a0d5b94..7c0c641 100644 --- a/test/encryption/ssss_test.dart +++ b/test/encryption/ssss_test.dart @@ -89,7 +89,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/fake_matrix_api.dart b/test/fake_matrix_api.dart index 2a149d4..6bbd87e 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -80,7 +80,7 @@ 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 { res = { @@ -748,7 +748,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 +1982,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 +2006,27 @@ 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%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 +2038,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 +2087,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/matrix_api_test.dart b/test/matrix_api_test.dart index a165628..7ad8ded 100644 --- a/test/matrix_api_test.dart +++ b/test/matrix_api_test.dart @@ -1377,7 +1377,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/timeline_test.dart b/test/timeline_test.dart index 005e8b0..385a67d 100644 --- a/test/timeline_test.dart +++ b/test/timeline_test.dart @@ -214,14 +214,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 +246,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 +266,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); + }); }); }