diff --git a/lib/src/client.dart b/lib/src/client.dart index c420b69f..d0372b17 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -451,7 +451,8 @@ class Client extends MatrixApi { Map get directChats => _accountData['m.direct']?.content ?? {}; - /// Returns the (first) room ID from the store which is a private chat with the user [userId]. + /// Returns the first room ID from the store (the room with the latest event) + /// which is a private chat with the user [userId]. /// Returns null if there is none. String? getDirectChatFromUserId(String userId) { final directChats = _accountData['m.direct']?.content[userId]; diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index cdc704dc..2aecf13a 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -77,15 +77,42 @@ class DeviceKeysList { } if (userId != client.userID) { // in-room verification with someone else - final roomId = await client.startDirectChat( - userId, - enableEncryption: newDirectChatEnableEncryption, - initialState: newDirectChatInitialState, - waitForSync: false, - ); + Room? room; + // we check if there's already a direct chat with the user + for (final directChatRoomId in client.directChats[userId] ?? []) { + final tempRoom = client.getRoomById(directChatRoomId); + if (tempRoom != null && + // check if the room is a direct chat and has less than 2 members + // (including the invited users) + (tempRoom.summary.mInvitedMemberCount ?? 0) + + (tempRoom.summary.mJoinedMemberCount ?? 1) <= + 2) { + // Now we check if the users in the room are none other than the current + // user and the user we want to verify + final members = tempRoom.getParticipants([ + Membership.invite, + Membership.join, + ]); + if (members.every((m) => {userId, client.userID}.contains(m.id))) { + // if so, we use that room + room = tempRoom; + break; + } + } + } + // if there's no direct chat that satisfies the conditions, we create a new one + if (room == null) { + final newRoomId = await client.startDirectChat( + userId, + enableEncryption: newDirectChatEnableEncryption, + initialState: newDirectChatInitialState, + waitForSync: false, + skipExistingChat: true, // to create a new room directly + ); + room = client.getRoomById(newRoomId) ?? + Room(id: newRoomId, client: client); + } - final room = - client.getRoomById(roomId) ?? Room(id: roomId, client: client); final request = KeyVerification(encryption: encryption, room: room, userId: userId); await request.start(); diff --git a/test/client_test.dart b/test/client_test.dart index f23844f4..0cf8a685 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -1273,6 +1273,23 @@ void main() { }); test('startDirectChat', () async { await matrix.startDirectChat('@alice:example.com', waitForSync: false); + + expect( + json.decode( + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last, + ), + { + 'initial_state': [ + { + 'content': {'algorithm': 'm.megolm.v1.aes-sha2'}, + 'type': 'm.room.encryption', + } + ], + 'invite': ['@alice:example.com'], + 'is_direct': true, + 'preset': 'trusted_private_chat', + }, + ); }); test('createGroupChat', () async { diff --git a/test/device_keys_list_test.dart b/test/device_keys_list_test.dart index ff3549af..e9e15ffe 100644 --- a/test/device_keys_list_test.dart +++ b/test/device_keys_list_test.dart @@ -275,10 +275,216 @@ void main() { expect(req != null, true); expect(req?.room != null, false); - req = await client.userDeviceKeys['@alice:example.com'] - ?.startVerification(newDirectChatEnableEncryption: false); - expect(req != null, true); - expect(req?.room != null, true); + final createRoomRequestCount = + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.length ?? 0; + + Future verifyDeviceKeys() async { + req = await client.userDeviceKeys['@alice:example.com'] + ?.startVerification(newDirectChatEnableEncryption: false); + expect(req != null, true); + expect(req?.room != null, true); + } + + await verifyDeviceKeys(); + // a new room should be created since there is no existing DM room + expect( + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.length, + createRoomRequestCount + 1, + ); + + await verifyDeviceKeys(); + // no new room should be created since the room already exists + expect( + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.length, + createRoomRequestCount + 1, + ); + + final dmRoomId = client.getDirectChatFromUserId('@alice:example.com'); + expect(dmRoomId != null, true); + final dmRoom = client.getRoomById(dmRoomId!); + expect(dmRoom != null, true); + // old state event should not overwrite current state events + dmRoom!.partial = false; + + // mock invite bob to the room + await client.handleSync( + SyncUpdate( + nextBatch: 'something', + rooms: RoomsUpdate( + join: { + dmRoomId: JoinedRoomUpdate( + state: [ + MatrixEvent( + type: EventTypes.RoomMember, + content: { + 'displayname': 'testclient', + 'is_direct': true, + 'membership': Membership.join.name, + }, + senderId: client.userID!, + eventId: 'eventId', + stateKey: client.userID!, + originServerTs: DateTime.now(), + ), + MatrixEvent( + type: EventTypes.RoomMember, + content: { + 'displayname': 'Bob the builder', + 'is_direct': true, + 'membership': Membership.invite.name, + }, + senderId: '@bob:example.com', + eventId: 'eventId', + stateKey: '@bob:example.com', + originServerTs: DateTime.now(), + ), + ], + summary: RoomSummary.fromJson({ + 'm.joined_member_count': 1, + 'm.invited_member_count': 1, + 'm.heroes': [], + }), + ), + }, + ), + ), + ); + expect( + dmRoom.getParticipants([Membership.invite, Membership.join]).length, + 2, + ); + dmRoom.partial = true; + + await verifyDeviceKeys(); + // a second room should now be created because bob(someone else other than + // alice) is invited into the first DM room + expect( + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.length, + createRoomRequestCount + 2, + ); + + final dmRoomId2 = client.getDirectChatFromUserId('@alice:example.com'); + expect(dmRoomId2 != null, true); + final dmRoom2 = client.getRoomById(dmRoomId2!); + expect(dmRoom2 != null, true); + // old state event should not overwrite current state events + dmRoom2!.partial = false; + + // mock invite alice and ban bob to the room + await client.handleSync( + SyncUpdate( + nextBatch: 'something', + rooms: RoomsUpdate( + join: { + dmRoomId2: JoinedRoomUpdate( + state: [ + MatrixEvent( + type: EventTypes.RoomMember, + content: { + 'displayname': 'Alice Catgirl', + 'is_direct': true, + 'membership': Membership.invite.name, + }, + senderId: '@alice:example.com', + eventId: 'eventId', + stateKey: '@alice:example.com', + originServerTs: DateTime.now(), + ), + MatrixEvent( + type: EventTypes.RoomMember, + content: { + 'displayname': 'Bob the builder', + 'is_direct': true, + 'membership': Membership.ban.name, + }, + senderId: '@bob:example.com', + eventId: 'eventId', + stateKey: '@bob:example.com', + originServerTs: DateTime.now(), + ), + ], + summary: RoomSummary.fromJson({ + 'm.joined_member_count': 1, + 'm.invited_member_count': 1, + 'm.heroes': [], + }), + ), + }, + ), + ), + ); + expect( + dmRoom2.getParticipants([Membership.invite, Membership.join]).length, + 2, + ); + dmRoom2.partial = true; + + await verifyDeviceKeys(); + // no new room should be created because only alice has been invited to the + // second room + expect( + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.length, + createRoomRequestCount + 2, + ); + + // old state event should not overwrite current state events + dmRoom2.partial = false; + // mock join alice and invite bob to the room + await client.handleSync( + SyncUpdate( + nextBatch: 'something', + rooms: RoomsUpdate( + join: { + dmRoomId2: JoinedRoomUpdate( + state: [ + MatrixEvent( + type: EventTypes.RoomMember, + content: { + 'displayname': 'Alice Catgirl', + 'is_direct': true, + 'membership': Membership.join.name, + }, + senderId: '@alice:example.com', + eventId: 'eventId', + stateKey: '@alice:example.com', + originServerTs: DateTime.now(), + ), + MatrixEvent( + type: EventTypes.RoomMember, + content: { + 'displayname': 'Bob the builder', + 'is_direct': true, + 'membership': Membership.invite.name, + }, + senderId: '@bob:example.com', + eventId: 'eventId', + stateKey: '@bob:example.com', + originServerTs: DateTime.now(), + ), + ], + summary: RoomSummary.fromJson({ + 'm.joined_member_count': 2, + 'm.invited_member_count': 1, + 'm.heroes': [], + }), + ), + }, + ), + ), + ); + expect( + dmRoom.getParticipants([Membership.invite, Membership.join]).length, + 3, + ); + dmRoom2.partial = true; + + await verifyDeviceKeys(); + // a third room should now be created because someone else (other than + // alice) is also invited into the second DM room + expect( + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.length, + createRoomRequestCount + 3, + ); }); test('dispose client', () async {