Skip to content

Commit f2e0820

Browse files
authored
runfix: 1:1 conversation with user without key packages (#17371) (#17412)
* refactor: update conversaiton readonly state * runfix: optionally do not allow unestablished mls 1:1 * feat: add a placeholder message for readonly 1:1 with a user without keys * runfix: update copy * test: readonly mls 1:1 no available keys
1 parent 5aefa1b commit f2e0820

File tree

6 files changed

+116
-25
lines changed

6 files changed

+116
-25
lines changed

src/i18n/en-US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,7 @@
11721172
"ongoingGroupVideoCall": "Ongoing video conference call with {{conversationName}}, your camera is {{cameraStatus}}.",
11731173
"ongoingVideoCall": "Ongoing video call with {{conversationName}}, your camera is {{cameraStatus}}.",
11741174
"otherUserNotSupportMLSMsg": "You can't communicate with {{participantName}} anymore, as you two now use different protocols. When {{participantName}} gets an update, you can call and send messages and files again.",
1175+
"otherUserNoAvailableKeyPackages": "You can't communicate with {{participantName}} at the moment. When {{participantName}} logs in again, you can call and send messages and files again.",
11751176
"participantDevicesDetailHeadline": "Verify that this matches the fingerprint shown on [bold]{{user}}’s device[/bold].",
11761177
"participantDevicesDetailHowTo": "How do I do that?",
11771178
"participantDevicesDetailResetSession": "Reset session",

src/script/components/Conversation/ReadOnlyConversationMessage/ReadOnlyConversationMessage.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,14 @@ describe('ReadOnlyConversationMessage', () => {
101101

102102
expect(getByText('conversationWithBlockedUser')).toBeDefined();
103103
});
104+
105+
it("renders a conversation with a user that don't have any key pakages available", () => {
106+
const conversation = generateConversation(CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_NO_KEY_PACKAGES, false);
107+
108+
const {getByText} = render(
109+
withTheme(<ReadOnlyConversationMessage reloadApp={() => {}} conversation={conversation} />),
110+
);
111+
112+
expect(getByText('otherUserNoAvailableKeyPackages')).toBeDefined();
113+
});
104114
});

src/script/components/Conversation/ReadOnlyConversationMessage/ReadOnlyConversationMessage.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,19 @@ export const ReadOnlyConversationMessage: FC<ReadOnlyConversationMessageProps> =
100100
</>
101101
</ReadOnlyConversationMessageBase>
102102
);
103+
case CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_NO_KEY_PACKAGES:
104+
return (
105+
<ReadOnlyConversationMessageBase>
106+
<span>
107+
{replaceReactComponents(t('otherUserNoAvailableKeyPackages'), [
108+
{
109+
exactMatch: '{{participantName}}',
110+
render: () => <strong>{user.name()}</strong>,
111+
},
112+
])}
113+
</span>
114+
</ReadOnlyConversationMessageBase>
115+
);
103116
}
104117
}
105118

src/script/conversation/ConversationRepository.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
} from '@wireapp/api-client/lib/event/';
4242
import {BackendError, BackendErrorLabel} from '@wireapp/api-client/lib/http';
4343
import {QualifiedId} from '@wireapp/api-client/lib/user';
44+
import {ClientMLSError, ClientMLSErrorLabel} from '@wireapp/core/lib/messagingProtocols/mls';
4445
import {amplify} from 'amplify';
4546
import {StatusCodes as HTTP_STATUS} from 'http-status-codes';
4647
import ko from 'knockout';
@@ -1019,6 +1020,55 @@ describe('ConversationRepository', () => {
10191020
);
10201021
});
10211022

1023+
it('marks mls 1:1 conversation as read-only if both users support mls but the other user has no keys available', async () => {
1024+
const conversationRepository = testFactory.conversation_repository!;
1025+
const userRepository = testFactory.user_repository!;
1026+
1027+
const otherUserId = {id: 'a718410c-3833-479d-bd80-a5df03f38414', domain: 'test-domain'};
1028+
const otherUser = new User(otherUserId.id, otherUserId.domain);
1029+
otherUser.supportedProtocols([ConversationProtocol.MLS]);
1030+
userRepository['userState'].users.push(otherUser);
1031+
1032+
const selfUserId = {id: '1a9da9ca-a495-47a8-ac70-9ffbe924b2d0', domain: 'test-domain'};
1033+
const selfUser = new User(selfUserId.id, selfUserId.domain);
1034+
selfUser.supportedProtocols([ConversationProtocol.MLS]);
1035+
jest.spyOn(conversationRepository['userState'], 'self').mockReturnValue(selfUser);
1036+
1037+
const mls1to1ConversationResponse = generateAPIConversation({
1038+
id: {id: '0aab891e-ccf1-4dba-9d74-bacec64b5b1e', domain: 'test-domain'},
1039+
type: CONVERSATION_TYPE.ONE_TO_ONE,
1040+
protocol: ConversationProtocol.MLS,
1041+
overwites: {group_id: 'groupId'},
1042+
}) as BackendMLSConversation;
1043+
1044+
const noKeysError = new ClientMLSError(ClientMLSErrorLabel.NO_KEY_PACKAGES_AVAILABLE);
1045+
1046+
jest
1047+
.spyOn(container.resolve(Core).service!.conversation, 'establishMLS1to1Conversation')
1048+
.mockRejectedValueOnce(noKeysError);
1049+
1050+
const [mls1to1Conversation] = conversationRepository.mapConversations([mls1to1ConversationResponse]);
1051+
1052+
const connection = new ConnectionEntity();
1053+
connection.conversationId = mls1to1Conversation.qualifiedId;
1054+
connection.userId = otherUserId;
1055+
otherUser.connection(connection);
1056+
mls1to1Conversation.connection(connection);
1057+
1058+
conversationRepository['conversationState'].conversations.push(mls1to1Conversation);
1059+
1060+
jest
1061+
.spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
1062+
.mockResolvedValueOnce(false);
1063+
1064+
const conversationEntity = await conversationRepository.getInitialised1To1Conversation(otherUser.qualifiedId);
1065+
1066+
expect(conversationEntity?.serialize()).toEqual(mls1to1Conversation.serialize());
1067+
expect(conversationEntity?.readOnlyState()).toEqual(
1068+
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_NO_KEY_PACKAGES,
1069+
);
1070+
});
1071+
10221072
it('deos not mark mls 1:1 conversation as read-only if the other user does not support mls but mls 1:1 was already established', async () => {
10231073
const conversationRepository = testFactory.conversation_repository!;
10241074
const userRepository = testFactory.user_repository!;

src/script/conversation/ConversationRepository.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {BackendErrorLabel} from '@wireapp/api-client/lib/http/';
4949
import type {BackendError} from '@wireapp/api-client/lib/http/';
5050
import type {QualifiedId} from '@wireapp/api-client/lib/user/';
5151
import {MLSCreateConversationResponse} from '@wireapp/core/lib/conversation';
52+
import {ClientMLSError, ClientMLSErrorLabel} from '@wireapp/core/lib/messagingProtocols/mls';
5253
import {amplify} from 'amplify';
5354
import {StatusCodes as HTTP_STATUS} from 'http-status-codes';
5455
import {container} from 'tsyringe';
@@ -171,11 +172,13 @@ type IncomingEvent = ConversationEvent | ClientConversationEvent;
171172
export enum CONVERSATION_READONLY_STATE {
172173
READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS = 'READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS',
173174
READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS = 'READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS',
175+
READONLY_ONE_TO_ONE_NO_KEY_PACKAGES = 'READONLY_ONE_TO_ONE_NO_KEY_PACKAGES',
174176
}
175177

176178
interface GetInitialised1To1ConversationOptions {
177179
isLiveUpdate?: boolean;
178180
shouldRefreshUser?: boolean;
181+
mls?: {allowUnestablished?: boolean};
179182
}
180183

181184
type ConversaitonWithServiceParams = {
@@ -1336,7 +1339,11 @@ export class ConversationRepository {
13361339
* We have to add a delay to make sure the welcome message is not wasted, in case the self client would establish mls group themselves before receiving the welcome.
13371340
*/
13381341
const shouldDelayMLSGroupEstablishment = options.isLiveUpdate && isMLSSupportedByTheOtherUser;
1339-
return this.initMLS1to1Conversation(userId, isMLSSupportedByTheOtherUser, shouldDelayMLSGroupEstablishment);
1342+
return this.initMLS1to1Conversation(userId, {
1343+
isMLSSupportedByTheOtherUser,
1344+
shouldDelayGroupEstablishment: shouldDelayMLSGroupEstablishment,
1345+
allowUnestablished: options.mls?.allowUnestablished,
1346+
});
13401347
}
13411348

13421349
// There's no connection so it's a proteus conversation with a team member
@@ -1514,14 +1521,6 @@ export class ConversationRepository {
15141521
}
15151522
};
15161523

1517-
private readonly updateConversationReadOnlyState = async (
1518-
conversationEntity: Conversation,
1519-
conversationReadOnlyState: CONVERSATION_READONLY_STATE | null,
1520-
) => {
1521-
conversationEntity.readOnlyState(conversationReadOnlyState);
1522-
await this.saveConversationStateInDb(conversationEntity);
1523-
};
1524-
15251524
private readonly getProtocolFor1to1Conversation = async (
15261525
otherUserId: QualifiedId,
15271526
shouldRefreshUser = false,
@@ -1759,8 +1758,11 @@ export class ConversationRepository {
17591758
*/
17601759
private readonly initMLS1to1Conversation = async (
17611760
otherUserId: QualifiedId,
1762-
isMLSSupportedByTheOtherUser: boolean,
1763-
shouldDelayGroupEstablishment = false,
1761+
{
1762+
isMLSSupportedByTheOtherUser,
1763+
shouldDelayGroupEstablishment = false,
1764+
allowUnestablished = true,
1765+
}: {isMLSSupportedByTheOtherUser: boolean; shouldDelayGroupEstablishment?: boolean; allowUnestablished?: boolean},
17641766
): Promise<MLSConversation> => {
17651767
// When receiving some live updates via websocket, e.g. after connection request is accepted, both sides (users) of connection will react to conversation status update event.
17661768
// We want to reduce the possibility of two users trying to establish an MLS group at the same time.
@@ -1819,11 +1821,24 @@ export class ConversationRepository {
18191821
throw new Error('Self user is not available!');
18201822
}
18211823

1822-
const initialisedMLSConversation = await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
1824+
let initialisedMLSConversation: MLSConversation = mlsConversation;
1825+
1826+
try {
1827+
initialisedMLSConversation = await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
1828+
initialisedMLSConversation.readOnlyState(null);
1829+
} catch (error) {
1830+
this.logger.warn(`Failed to establish MLS 1:1 conversation with user ${otherUserId.id}`, error);
1831+
if (!allowUnestablished) {
1832+
throw error;
1833+
}
1834+
1835+
if (error instanceof ClientMLSError && error.label === ClientMLSErrorLabel.NO_KEY_PACKAGES_AVAILABLE) {
1836+
initialisedMLSConversation.readOnlyState(CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_NO_KEY_PACKAGES);
1837+
}
1838+
}
18231839

18241840
// If mls is supported by the other user, we can establish the group and remove readonly state from the conversation.
1825-
initialisedMLSConversation.readOnlyState(null);
1826-
await this.update1To1ConversationParticipants(mlsConversation, otherUserId);
1841+
await this.update1To1ConversationParticipants(initialisedMLSConversation, otherUserId);
18271842
await this.saveConversation(initialisedMLSConversation);
18281843

18291844
if (shouldOpenMLS1to1Conversation) {
@@ -1868,16 +1883,13 @@ export class ConversationRepository {
18681883
// If proteus is not supported by the other user we have to mark conversation as readonly
18691884
if (!doesOtherUserSupportProteus) {
18701885
await this.blacklistConversation(proteusConversationId);
1871-
await this.updateConversationReadOnlyState(
1872-
proteusConversation,
1873-
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS,
1874-
);
1886+
proteusConversation.readOnlyState(CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS);
18751887
return proteusConversation;
18761888
}
18771889

18781890
// If proteus is supported by the other user, we just return a proteus conversation and remove readonly state from it.
18791891
await this.removeConversationFromBlacklist(proteusConversationId);
1880-
await this.updateConversationReadOnlyState(proteusConversation, null);
1892+
await proteusConversation.readOnlyState(null);
18811893
return proteusConversation;
18821894
};
18831895

@@ -1992,11 +2004,10 @@ export class ConversationRepository {
19922004
`Connection with user ${otherUserId.id} is accepted, using protocol ${protocol} for 1:1 conversation`,
19932005
);
19942006
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
1995-
return this.initMLS1to1Conversation(
1996-
otherUserId,
2007+
return this.initMLS1to1Conversation(otherUserId, {
19972008
isMLSSupportedByTheOtherUser,
1998-
shouldDelayMLSGroupEstablishment,
1999-
);
2009+
shouldDelayGroupEstablishment: shouldDelayMLSGroupEstablishment,
2010+
});
20002011
}
20012012

20022013
if (protocol === ConversationProtocol.PROTEUS) {
@@ -2013,7 +2024,10 @@ export class ConversationRepository {
20132024
this.logger.log(
20142025
`Connection with user ${otherUserId.id} is not accepted, using already known MLS 1:1 conversation ${localMLSConversation.id}`,
20152026
);
2016-
return this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser, shouldDelayMLSGroupEstablishment);
2027+
return this.initMLS1to1Conversation(otherUserId, {
2028+
isMLSSupportedByTheOtherUser,
2029+
shouldDelayGroupEstablishment: shouldDelayMLSGroupEstablishment,
2030+
});
20172031
}
20182032

20192033
this.logger.log(

src/script/view_model/ActionsViewModel.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,10 @@ export class ActionsViewModel {
330330
};
331331

332332
getOrCreate1to1Conversation = async (userEntity: User): Promise<Conversation> => {
333-
const conversationEntity = await this.conversationRepository.getInitialised1To1Conversation(userEntity.qualifiedId);
333+
const conversationEntity = await this.conversationRepository.getInitialised1To1Conversation(
334+
userEntity.qualifiedId,
335+
{mls: {allowUnestablished: false}},
336+
);
334337
if (conversationEntity) {
335338
return conversationEntity;
336339
}

0 commit comments

Comments
 (0)