From 15dce00c0fd77be312e05e4960d7f8bc4e1b7962 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 17 Sep 2024 19:14:45 +0200 Subject: [PATCH] fix: don't convert archived rooms to joined rooms by accident When decrypting the last event for archived rooms when a room key is received, we used to send a synthetic SyncUpdate. This however put the archived room into the join section, which converted the room to a joined room. We need to respect what section the room was in when sending synthetic SyncUpdates. fixes https://github.com/famedly/matrix-dart-sdk/issues/1916 --- lib/encryption/key_manager.dart | 16 +++++- lib/matrix_api_lite/model/sync_update.dart | 29 ++++++++++ test/room_archived_test.dart | 62 +++++++++++++++++++++- 3 files changed, 104 insertions(+), 3 deletions(-) diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index e3003a37..5386b763 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -164,6 +164,7 @@ class KeyManager { if (!client.isLogged() || client.encryption == null) { return; } + final storeFuture = client.database ?.storeInboundGroupSession( roomId, @@ -200,8 +201,19 @@ class KeyManager { await client.database?.transaction(() async { await client.handleSync( SyncUpdate( - nextBatch: '', - rooms: RoomsUpdate(join: {room.id: JoinedRoomUpdate()})), + nextBatch: '', + rooms: switch (room.membership) { + Membership.join => + RoomsUpdate(join: {room.id: JoinedRoomUpdate()}), + Membership.ban || + Membership.leave => + RoomsUpdate(leave: {room.id: LeftRoomUpdate()}), + Membership.invite => + RoomsUpdate(invite: {room.id: InvitedRoomUpdate()}), + Membership.knock => + RoomsUpdate(knock: {room.id: KnockRoomUpdate()}), + }, + ), ); }); } diff --git a/lib/matrix_api_lite/model/sync_update.dart b/lib/matrix_api_lite/model/sync_update.dart index 25ff2266..24a4b2e9 100644 --- a/lib/matrix_api_lite/model/sync_update.dart +++ b/lib/matrix_api_lite/model/sync_update.dart @@ -114,11 +114,13 @@ class RoomsUpdate { Map? join; Map? invite; Map? leave; + Map? knock; RoomsUpdate({ this.join, this.invite, this.leave, + this.knock, }); RoomsUpdate.fromJson(Map json) { @@ -128,6 +130,8 @@ class RoomsUpdate { MapEntry(k, InvitedRoomUpdate.fromJson(v as Map))); leave = json.tryGetMap('leave')?.catchMap((k, v) => MapEntry(k, LeftRoomUpdate.fromJson(v as Map))); + knock = json.tryGetMap('knock')?.catchMap((k, v) => + MapEntry(k, KnockRoomUpdate.fromJson(v as Map))); } Map toJson() { @@ -141,6 +145,9 @@ class RoomsUpdate { if (leave != null) { data['leave'] = leave!.map((k, v) => MapEntry(k, v.toJson())); } + if (knock != null) { + data['knock'] = knock!.map((k, v) => MapEntry(k, v.toJson())); + } return data; } } @@ -234,6 +241,28 @@ class InvitedRoomUpdate extends SyncRoomUpdate { } } +class KnockRoomUpdate extends SyncRoomUpdate { + List? knockState; + + KnockRoomUpdate({this.knockState}); + + KnockRoomUpdate.fromJson(Map json) + : knockState = json + .tryGetMap>('knock_state')?['events'] + ?.map((i) => StrippedStateEvent.fromJson(i as Map)) + .toList(); + + Map toJson() { + final data = {}; + if (knockState != null) { + data['knock_state'] = { + 'events': knockState!.map((i) => i.toJson()).toList(), + }; + } + return data; + } +} + class LeftRoomUpdate extends SyncRoomUpdate { List? state; TimelineUpdate? timeline; diff --git a/test/room_archived_test.dart b/test/room_archived_test.dart index b21d9caf..22fc0816 100644 --- a/test/room_archived_test.dart +++ b/test/room_archived_test.dart @@ -23,7 +23,7 @@ import 'package:test/test.dart'; import 'package:matrix/matrix.dart'; import 'fake_client.dart'; -void main() { +void main() async { group('Timeline', () { Logs().level = Level.error; @@ -115,6 +115,66 @@ void main() { expect(client.getArchiveRoomFromCache('!5345234235:example.com'), null); }); + test("assert that key updates don't change membership", () async { + const roomid = '!5345234235:example.com'; + + // prep work to be able to set a last event that would trigger the (fixed) bug + await client.loadArchiveWithTimeline(); + expect(client.getArchiveRoomFromCache(roomid) != null, true); + expect(client.getRoomById(roomid)?.membership, Membership.leave); + + final outboundSession = await client.encryption?.keyManager + .createOutboundGroupSession(roomid); + final inboundSession = client.encryption!.keyManager + .getInboundGroupSession( + roomid, outboundSession!.outboundGroupSession!.session_id())!; + + // ensure encryption is "enabled" + client.getRoomById(roomid)?.setState(StrippedStateEvent( + type: EventTypes.Encryption, + content: {'algorithm': AlgorithmTypes.megolmV1AesSha2}, + senderId: client.userID!, + stateKey: '', + )); + final encryptedEvent = await client.encryption! + .encryptGroupMessagePayload( + roomid, {'msgtype': 'm.room.text', 'body': 'empty'}); + + // reset client + await client.dispose().onError((e, s) {}); + client = await getClient( + sendTimelineEventTimeout: const Duration(seconds: 5), + ); + + await client.abortSync(); + insertList.clear(); + + // now do our tests + await client.loadArchiveWithTimeline(); + expect(client.getArchiveRoomFromCache(roomid) != null, true); + expect(client.getRoomById(roomid)?.membership, Membership.leave); + + // set the last event + final room = client.getRoomById(roomid)!; + room.lastEvent = Event( + type: EventTypes.Encrypted, + content: encryptedEvent, + senderId: client.userID!, + eventId: '\$archivedencr', + room: room, + originServerTs: DateTime.now()); + + // import the inbound session + await client.encryption!.keyManager.setInboundGroupSession( + roomid, + inboundSession.sessionId, + inboundSession.senderKey, + inboundSession.content); + + expect(client.getArchiveRoomFromCache(roomid) != null, true); + expect(client.getRoomById(roomid)?.membership, Membership.leave); + }, tags: 'olm'); + test('clear archive', () async { await client.loadArchiveWithTimeline(); client.clearArchivesFromCache();