Skip to content

Commit 8dfc2cd

Browse files
authored
runfix: 1:1 conversation with user without key packages (#17371)
* 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 1588a84 commit 8dfc2cd

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
@@ -1167,6 +1167,7 @@
11671167
"ongoingGroupVideoCall": "Ongoing video conference call with {{conversationName}}, your camera is {{cameraStatus}}.",
11681168
"ongoingVideoCall": "Ongoing video call with {{conversationName}}, your camera is {{cameraStatus}}.",
11691169
"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.",
1170+
"otherUserNoAvailableKeyPackages": "You can't communicate with {{participantName}} at the moment. When {{participantName}} logs in again, you can call and send messages and files again.",
11701171
"participantDevicesDetailHeadline": "Verify that this matches the fingerprint shown on [bold]{{user}}’s device[/bold].",
11711172
"participantDevicesDetailHowTo": "How do I do that?",
11721173
"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
@@ -91,4 +91,14 @@ describe('ReadOnlyConversationMessage', () => {
9191

9292
expect(reloadAppMock).toHaveBeenCalled();
9393
});
94+
95+
it("renders a conversation with a user that don't have any key pakages available", () => {
96+
const conversation = generateConversation(CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_NO_KEY_PACKAGES, true);
97+
98+
const {getByText} = render(
99+
withTheme(<ReadOnlyConversationMessage reloadApp={() => {}} conversation={conversation} />),
100+
);
101+
102+
expect(getByText('otherUserNoAvailableKeyPackages')).toBeDefined();
103+
});
94104
});

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@ export const ReadOnlyConversationMessage: FC<ReadOnlyConversationMessageProps> =
8787
</>
8888
</ReadOnlyConversationMessageBase>
8989
);
90+
case CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_NO_KEY_PACKAGES:
91+
return (
92+
<ReadOnlyConversationMessageBase>
93+
<span>
94+
{replaceReactComponents(t('otherUserNoAvailableKeyPackages'), [
95+
{
96+
exactMatch: '{{participantName}}',
97+
render: () => <strong>{user.name()}</strong>,
98+
},
99+
])}
100+
</span>
101+
</ReadOnlyConversationMessageBase>
102+
);
90103
}
91104
}
92105

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';
@@ -915,6 +916,55 @@ describe('ConversationRepository', () => {
915916
);
916917
});
917918

919+
it('marks mls 1:1 conversation as read-only if both users support mls but the other user has no keys available', async () => {
920+
const conversationRepository = testFactory.conversation_repository!;
921+
const userRepository = testFactory.user_repository!;
922+
923+
const otherUserId = {id: 'a718410c-3833-479d-bd80-a5df03f38414', domain: 'test-domain'};
924+
const otherUser = new User(otherUserId.id, otherUserId.domain);
925+
otherUser.supportedProtocols([ConversationProtocol.MLS]);
926+
userRepository['userState'].users.push(otherUser);
927+
928+
const selfUserId = {id: '1a9da9ca-a495-47a8-ac70-9ffbe924b2d0', domain: 'test-domain'};
929+
const selfUser = new User(selfUserId.id, selfUserId.domain);
930+
selfUser.supportedProtocols([ConversationProtocol.MLS]);
931+
jest.spyOn(conversationRepository['userState'], 'self').mockReturnValue(selfUser);
932+
933+
const mls1to1ConversationResponse = generateAPIConversation({
934+
id: {id: '0aab891e-ccf1-4dba-9d74-bacec64b5b1e', domain: 'test-domain'},
935+
type: CONVERSATION_TYPE.ONE_TO_ONE,
936+
protocol: ConversationProtocol.MLS,
937+
overwites: {group_id: 'groupId'},
938+
}) as BackendMLSConversation;
939+
940+
const noKeysError = new ClientMLSError(ClientMLSErrorLabel.NO_KEY_PACKAGES_AVAILABLE);
941+
942+
jest
943+
.spyOn(container.resolve(Core).service!.conversation, 'establishMLS1to1Conversation')
944+
.mockRejectedValueOnce(noKeysError);
945+
946+
const [mls1to1Conversation] = conversationRepository.mapConversations([mls1to1ConversationResponse]);
947+
948+
const connection = new ConnectionEntity();
949+
connection.conversationId = mls1to1Conversation.qualifiedId;
950+
connection.userId = otherUserId;
951+
otherUser.connection(connection);
952+
mls1to1Conversation.connection(connection);
953+
954+
conversationRepository['conversationState'].conversations.push(mls1to1Conversation);
955+
956+
jest
957+
.spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
958+
.mockResolvedValueOnce(false);
959+
960+
const conversationEntity = await conversationRepository.getInitialised1To1Conversation(otherUser.qualifiedId);
961+
962+
expect(conversationEntity?.serialize()).toEqual(mls1to1Conversation.serialize());
963+
expect(conversationEntity?.readOnlyState()).toEqual(
964+
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_NO_KEY_PACKAGES,
965+
);
966+
});
967+
918968
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 () => {
919969
const conversationRepository = testFactory.conversation_repository!;
920970
const userRepository = testFactory.user_repository!;

src/script/conversation/ConversationRepository.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {BackendErrorLabel} from '@wireapp/api-client/lib/http/';
4545
import type {BackendError} from '@wireapp/api-client/lib/http/';
4646
import type {QualifiedId} from '@wireapp/api-client/lib/user/';
4747
import {MLSCreateConversationResponse} from '@wireapp/core/lib/conversation';
48+
import {ClientMLSError, ClientMLSErrorLabel} from '@wireapp/core/lib/messagingProtocols/mls';
4849
import {amplify} from 'amplify';
4950
import {StatusCodes as HTTP_STATUS} from 'http-status-codes';
5051
import {container} from 'tsyringe';
@@ -167,11 +168,13 @@ type IncomingEvent = ConversationEvent | ClientConversationEvent;
167168
export enum CONVERSATION_READONLY_STATE {
168169
READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS = 'READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS',
169170
READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS = 'READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS',
171+
READONLY_ONE_TO_ONE_NO_KEY_PACKAGES = 'READONLY_ONE_TO_ONE_NO_KEY_PACKAGES',
170172
}
171173

172174
interface GetInitialised1To1ConversationOptions {
173175
isLiveUpdate?: boolean;
174176
shouldRefreshUser?: boolean;
177+
mls?: {allowUnestablished?: boolean};
175178
}
176179

177180
export class ConversationRepository {
@@ -1325,7 +1328,11 @@ export class ConversationRepository {
13251328
* 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.
13261329
*/
13271330
const shouldDelayMLSGroupEstablishment = options.isLiveUpdate && isMLSSupportedByTheOtherUser;
1328-
return this.initMLS1to1Conversation(userId, isMLSSupportedByTheOtherUser, shouldDelayMLSGroupEstablishment);
1331+
return this.initMLS1to1Conversation(userId, {
1332+
isMLSSupportedByTheOtherUser,
1333+
shouldDelayGroupEstablishment: shouldDelayMLSGroupEstablishment,
1334+
allowUnestablished: options.mls?.allowUnestablished,
1335+
});
13291336
}
13301337

13311338
// There's no connection so it's a proteus conversation with a team member
@@ -1503,14 +1510,6 @@ export class ConversationRepository {
15031510
}
15041511
};
15051512

1506-
private readonly updateConversationReadOnlyState = async (
1507-
conversationEntity: Conversation,
1508-
conversationReadOnlyState: CONVERSATION_READONLY_STATE | null,
1509-
) => {
1510-
conversationEntity.readOnlyState(conversationReadOnlyState);
1511-
await this.saveConversationStateInDb(conversationEntity);
1512-
};
1513-
15141513
private readonly getProtocolFor1to1Conversation = async (
15151514
otherUserId: QualifiedId,
15161515
shouldRefreshUser = false,
@@ -1748,8 +1747,11 @@ export class ConversationRepository {
17481747
*/
17491748
private readonly initMLS1to1Conversation = async (
17501749
otherUserId: QualifiedId,
1751-
isMLSSupportedByTheOtherUser: boolean,
1752-
shouldDelayGroupEstablishment = false,
1750+
{
1751+
isMLSSupportedByTheOtherUser,
1752+
shouldDelayGroupEstablishment = false,
1753+
allowUnestablished = true,
1754+
}: {isMLSSupportedByTheOtherUser: boolean; shouldDelayGroupEstablishment?: boolean; allowUnestablished?: boolean},
17531755
): Promise<MLSConversation> => {
17541756
// 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.
17551757
// We want to reduce the possibility of two users trying to establish an MLS group at the same time.
@@ -1808,11 +1810,24 @@ export class ConversationRepository {
18081810
throw new Error('Self user is not available!');
18091811
}
18101812

1811-
const initialisedMLSConversation = await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
1813+
let initialisedMLSConversation: MLSConversation = mlsConversation;
1814+
1815+
try {
1816+
initialisedMLSConversation = await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
1817+
initialisedMLSConversation.readOnlyState(null);
1818+
} catch (error) {
1819+
this.logger.warn(`Failed to establish MLS 1:1 conversation with user ${otherUserId.id}`, error);
1820+
if (!allowUnestablished) {
1821+
throw error;
1822+
}
1823+
1824+
if (error instanceof ClientMLSError && error.label === ClientMLSErrorLabel.NO_KEY_PACKAGES_AVAILABLE) {
1825+
initialisedMLSConversation.readOnlyState(CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_NO_KEY_PACKAGES);
1826+
}
1827+
}
18121828

18131829
// If mls is supported by the other user, we can establish the group and remove readonly state from the conversation.
1814-
initialisedMLSConversation.readOnlyState(null);
1815-
await this.update1To1ConversationParticipants(mlsConversation, otherUserId);
1830+
await this.update1To1ConversationParticipants(initialisedMLSConversation, otherUserId);
18161831
await this.saveConversation(initialisedMLSConversation);
18171832

18181833
if (shouldOpenMLS1to1Conversation) {
@@ -1857,16 +1872,13 @@ export class ConversationRepository {
18571872
// If proteus is not supported by the other user we have to mark conversation as readonly
18581873
if (!doesOtherUserSupportProteus) {
18591874
await this.blacklistConversation(proteusConversationId);
1860-
await this.updateConversationReadOnlyState(
1861-
proteusConversation,
1862-
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS,
1863-
);
1875+
proteusConversation.readOnlyState(CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS);
18641876
return proteusConversation;
18651877
}
18661878

18671879
// If proteus is supported by the other user, we just return a proteus conversation and remove readonly state from it.
18681880
await this.removeConversationFromBlacklist(proteusConversationId);
1869-
await this.updateConversationReadOnlyState(proteusConversation, null);
1881+
await proteusConversation.readOnlyState(null);
18701882
return proteusConversation;
18711883
};
18721884

@@ -1975,11 +1987,10 @@ export class ConversationRepository {
19751987
`Connection with user ${otherUserId.id} is accepted, using protocol ${protocol} for 1:1 conversation`,
19761988
);
19771989
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
1978-
return this.initMLS1to1Conversation(
1979-
otherUserId,
1990+
return this.initMLS1to1Conversation(otherUserId, {
19801991
isMLSSupportedByTheOtherUser,
1981-
shouldDelayMLSGroupEstablishment,
1982-
);
1992+
shouldDelayGroupEstablishment: shouldDelayMLSGroupEstablishment,
1993+
});
19831994
}
19841995

19851996
if (protocol === ConversationProtocol.PROTEUS) {
@@ -1996,7 +2007,10 @@ export class ConversationRepository {
19962007
this.logger.log(
19972008
`Connection with user ${otherUserId.id} is not accepted, using already known MLS 1:1 conversation ${localMLSConversation.id}`,
19982009
);
1999-
return this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser, shouldDelayMLSGroupEstablishment);
2010+
return this.initMLS1to1Conversation(otherUserId, {
2011+
isMLSSupportedByTheOtherUser,
2012+
shouldDelayGroupEstablishment: shouldDelayMLSGroupEstablishment,
2013+
});
20002014
}
20012015

20022016
this.logger.log(

src/script/view_model/ActionsViewModel.ts

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

338338
getOrCreate1to1Conversation = async (userEntity: User): Promise<Conversation> => {
339-
const conversationEntity = await this.conversationRepository.getInitialised1To1Conversation(userEntity.qualifiedId);
339+
const conversationEntity = await this.conversationRepository.getInitialised1To1Conversation(
340+
userEntity.qualifiedId,
341+
{mls: {allowUnestablished: false}},
342+
);
340343
if (conversationEntity) {
341344
return conversationEntity;
342345
}

0 commit comments

Comments
 (0)