Merge pull request #2030 from famedly/karthi/ensureIsOnlyTwoMembers

chore: ensure direct chats have only 2 members before sending verification requests
This commit is contained in:
Karthikeyan S 2025-03-26 15:22:03 +05:30 committed by GitHub
commit c63e89f667
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 264 additions and 13 deletions

View File

@ -451,7 +451,8 @@ class Client extends MatrixApi {
Map<String, dynamic> 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];

View File

@ -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();

View File

@ -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 {

View File

@ -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<void> 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 {