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
This commit is contained in:
Nicolas Werner 2024-09-17 19:14:45 +02:00
parent fec258ef12
commit 15dce00c0f
No known key found for this signature in database
GPG Key ID: B38119FF80087618
3 changed files with 104 additions and 3 deletions

View File

@ -164,6 +164,7 @@ class KeyManager {
if (!client.isLogged() || client.encryption == null) { if (!client.isLogged() || client.encryption == null) {
return; return;
} }
final storeFuture = client.database final storeFuture = client.database
?.storeInboundGroupSession( ?.storeInboundGroupSession(
roomId, roomId,
@ -200,8 +201,19 @@ class KeyManager {
await client.database?.transaction(() async { await client.database?.transaction(() async {
await client.handleSync( await client.handleSync(
SyncUpdate( SyncUpdate(
nextBatch: '', nextBatch: '',
rooms: RoomsUpdate(join: {room.id: JoinedRoomUpdate()})), 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()}),
},
),
); );
}); });
} }

View File

@ -114,11 +114,13 @@ class RoomsUpdate {
Map<String, JoinedRoomUpdate>? join; Map<String, JoinedRoomUpdate>? join;
Map<String, InvitedRoomUpdate>? invite; Map<String, InvitedRoomUpdate>? invite;
Map<String, LeftRoomUpdate>? leave; Map<String, LeftRoomUpdate>? leave;
Map<String, KnockRoomUpdate>? knock;
RoomsUpdate({ RoomsUpdate({
this.join, this.join,
this.invite, this.invite,
this.leave, this.leave,
this.knock,
}); });
RoomsUpdate.fromJson(Map<String, Object?> json) { RoomsUpdate.fromJson(Map<String, Object?> json) {
@ -128,6 +130,8 @@ class RoomsUpdate {
MapEntry(k, InvitedRoomUpdate.fromJson(v as Map<String, Object?>))); MapEntry(k, InvitedRoomUpdate.fromJson(v as Map<String, Object?>)));
leave = json.tryGetMap<String, Object?>('leave')?.catchMap((k, v) => leave = json.tryGetMap<String, Object?>('leave')?.catchMap((k, v) =>
MapEntry(k, LeftRoomUpdate.fromJson(v as Map<String, Object?>))); MapEntry(k, LeftRoomUpdate.fromJson(v as Map<String, Object?>)));
knock = json.tryGetMap<String, Object?>('knock')?.catchMap((k, v) =>
MapEntry(k, KnockRoomUpdate.fromJson(v as Map<String, Object?>)));
} }
Map<String, Object?> toJson() { Map<String, Object?> toJson() {
@ -141,6 +145,9 @@ class RoomsUpdate {
if (leave != null) { if (leave != null) {
data['leave'] = leave!.map((k, v) => MapEntry(k, v.toJson())); 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; return data;
} }
} }
@ -234,6 +241,28 @@ class InvitedRoomUpdate extends SyncRoomUpdate {
} }
} }
class KnockRoomUpdate extends SyncRoomUpdate {
List<StrippedStateEvent>? knockState;
KnockRoomUpdate({this.knockState});
KnockRoomUpdate.fromJson(Map<String, Object?> json)
: knockState = json
.tryGetMap<String, List<Object?>>('knock_state')?['events']
?.map((i) => StrippedStateEvent.fromJson(i as Map<String, Object?>))
.toList();
Map<String, Object?> toJson() {
final data = <String, Object?>{};
if (knockState != null) {
data['knock_state'] = {
'events': knockState!.map((i) => i.toJson()).toList(),
};
}
return data;
}
}
class LeftRoomUpdate extends SyncRoomUpdate { class LeftRoomUpdate extends SyncRoomUpdate {
List<MatrixEvent>? state; List<MatrixEvent>? state;
TimelineUpdate? timeline; TimelineUpdate? timeline;

View File

@ -23,7 +23,7 @@ import 'package:test/test.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'fake_client.dart'; import 'fake_client.dart';
void main() { void main() async {
group('Timeline', () { group('Timeline', () {
Logs().level = Level.error; Logs().level = Level.error;
@ -115,6 +115,66 @@ void main() {
expect(client.getArchiveRoomFromCache('!5345234235:example.com'), null); 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 { test('clear archive', () async {
await client.loadArchiveWithTimeline(); await client.loadArchiveWithTimeline();
client.clearArchivesFromCache(); client.clearArchivesFromCache();