Skip to content

Commit

Permalink
feat: one2one migrated to mls system message [WPB-6195] (#16560) (#16590
Browse files Browse the repository at this point in the history
)

* feat: add system message for 1:1 conversation migrated to mls

* test: check if message was injected

* refactor: improve naming

* refactor: add check for connection request to conversation selectors

* runfix: change type to 1:1 only in init proteus method

* runfix: always blacklist proteus 1:1 if its being migrated to mls

* refactor: improve naming

* docs: type



---------

Co-authored-by: Patryk Górka <[email protected]>
Co-authored-by: Thomas Belin <[email protected]>
  • Loading branch information
3 people authored Jan 23, 2024
1 parent f69f66c commit 6d34620
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {JoinedAfterMLSMigrationFinalisationMessage} from 'src/script/entity/mess
import {MessageTimerUpdateMessage} from 'src/script/entity/message/MessageTimerUpdateMessage';
import {MLSConversationRecoveredMessage} from 'src/script/entity/message/MLSConversationRecoveredMessage';
import {MLSMigrationFinalisationOngoingCallMessage} from 'src/script/entity/message/MLSMigrationFinalisationOngoingCallMessage';
import {OneToOneMigratedToMlsMessage} from 'src/script/entity/message/OneToOneMigratedToMlsMessage';
import {ProtocolUpdateMessage} from 'src/script/entity/message/ProtocolUpdateMessage';
import {ReceiptModeUpdateMessage} from 'src/script/entity/message/ReceiptModeUpdateMessage';
import {RenameMessage} from 'src/script/entity/message/RenameMessage';
Expand Down Expand Up @@ -74,6 +75,10 @@ export const SystemMessage: React.FC<SystemMessageProps> = ({message}) => {
return <SystemMessageBase message={message} icon={<Icon.Info />} />;
}

if (message instanceof OneToOneMigratedToMlsMessage) {
return <SystemMessageBase message={message} icon={<Icon.Info />} />;
}

if (message instanceof MLSMigrationFinalisationOngoingCallMessage) {
return <SystemMessageBase message={message} icon={<Icon.Info />} />;
}
Expand Down
2 changes: 2 additions & 0 deletions src/script/conversation/ConversationRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ describe('ConversationRepository', () => {
.mockReturnValueOnce(proteus1to1Conversation);
jest.spyOn(conversationRepository['conversationService'], 'deleteConversationFromDb');
jest.spyOn(conversationRepository['conversationService'], 'blacklistConversation');
jest.spyOn(conversationRepository['eventRepository'], 'injectEvent').mockResolvedValueOnce(undefined);

const conversationEntity = await conversationRepository.getInitialised1To1Conversation(otherUser);

Expand All @@ -560,6 +561,7 @@ describe('ConversationRepository', () => {
proteus1to1Conversation.qualifiedId,
);
expect(container.resolve(Core).service!.conversation.establishMLS1to1Conversation).toHaveBeenCalled();
expect(conversationRepository['eventRepository'].injectEvent).toHaveBeenCalled();
expect(conversationRepository['conversationState'].conversations()).not.toEqual(
expect.arrayContaining([proteus1to1Conversation]),
);
Expand Down
43 changes: 34 additions & 9 deletions src/script/conversation/ConversationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
MLSConversation,
isProteus1to1ConversationWithUser,
ProteusConversation,
isConnectionRequestConversation,
} from './ConversationSelectors';
import {ConversationService} from './ConversationService';
import {ConversationState} from './ConversationState';
Expand Down Expand Up @@ -1407,13 +1408,21 @@ export class ConversationRepository {
* @param mlsConversation - mls 1:1 conversation
* @returns {shouldOpenMLS1to1Conversation, wasProteus1to1Replaced} - whether proteus 1:1 was replaced with mls and whether it was an active conversation and mls 1:1 conversation should be opened in the UI
*/
private readonly replaceProteus1to1WithMLS = async (
private readonly migrateProteus1to1MLS = async (
otherUserId: QualifiedId,
mlsConversation: MLSConversation,
): Promise<{shouldOpenMLS1to1Conversation: boolean; wasProteus1to1Replaced: boolean}> => {
const proteusConversations = this.conversationState.findProteus1to1Conversations(otherUserId);

if (!proteusConversations || proteusConversations.length < 1) {
// Even if we don't have proteus 1:1 conversation, we still want to blacklist the proteus 1:1 conversation
// which is by default assigned to connection entity by backend (so it's not being fetched anymore).
const otherUser = this.userRepository.findUserById(otherUserId);
const conversationId = otherUser?.connection()?.conversationId;

if (conversationId) {
await this.blacklistConversation(conversationId);
}
return {shouldOpenMLS1to1Conversation: false, wasProteus1to1Replaced: false};
}

Expand Down Expand Up @@ -1464,6 +1473,12 @@ export class ConversationRepository {

ConversationMapper.updateProperties(mlsConversation, updates);

const wasProteus1to1ActiveConversation = proteusConversations.some(conversation =>
this.conversationState.isActiveConversation(conversation),
);

const wasProteusConnectionIncomingRequest = proteusConversations.some(isConnectionRequestConversation);

await Promise.allSettled(
proteusConversations.map(async proteusConversation => {
this.logger.info(`Deleting proteus 1:1 conversation ${proteusConversation.id}`);
Expand All @@ -1472,9 +1487,11 @@ export class ConversationRepository {
}),
);

const wasProteus1to1ActiveConversation =
!!proteusConversations &&
proteusConversations.some(conversation => this.conversationState.isActiveConversation(conversation));
// Because of the current architecture and the fact that we present a connection request as a conversation of connect type,
// we don't want to inject conversation migrated event if the only proteus 1:1 conversation we had was a connection request.
if (!wasProteusConnectionIncomingRequest) {
await this.inject1to1MigratedToMLS(mlsConversation);
}

const isMLS1to1ActiveConversation = this.conversationState.isActiveConversation(mlsConversation);

Expand Down Expand Up @@ -1595,7 +1612,7 @@ export class ConversationRepository {
}

// If proteus 1:1 conversation with the same user is known, we have to make sure it is replaced with mls 1:1 conversation.
const {shouldOpenMLS1to1Conversation, wasProteus1to1Replaced} = await this.replaceProteus1to1WithMLS(
const {shouldOpenMLS1to1Conversation, wasProteus1to1Replaced} = await this.migrateProteus1to1MLS(
otherUserId,
mlsConversation,
);
Expand Down Expand Up @@ -1677,6 +1694,12 @@ export class ConversationRepository {
throw new Error('initProteus1to1Conversation provided with conversation id of conversation that is not proteus');
}

const connection = proteusConversation.connection();

if (connection && connection.isConnected()) {
proteusConversation.type(CONVERSATION_TYPE.ONE_TO_ONE);
}

// If proteus is not supported by the other user we have to mark conversation as readonly
if (!doesOtherUserSupportProteus) {
await this.blacklistConversation(proteusConversationId);
Expand Down Expand Up @@ -1852,10 +1875,6 @@ export class ConversationRepository {

conversation.connection(connectionEntity);

if (connectionEntity.isConnected()) {
conversation.type(CONVERSATION_TYPE.ONE_TO_ONE);
}

const updatedConversation = await this.updateParticipatingUserEntities(conversation);

this.conversationState.conversations.notifySubscribers();
Expand Down Expand Up @@ -2385,6 +2404,11 @@ export class ConversationRepository {
return undefined;
}

private readonly inject1to1MigratedToMLS = async (conversation: Conversation) => {
const protocolUpdateEvent = EventBuilder.build1to1MigratedToMLS(conversation);
await this.eventRepository.injectEvent(protocolUpdateEvent);
};

/**
* Update conversation protocol
* This will update the protocol of the conversation and refetch the conversation to get all new fields (groupId, ciphersuite, epoch and new protocol)
Expand Down Expand Up @@ -3058,6 +3082,7 @@ export class ConversationRepository {
case ClientEvent.CONVERSATION.JOINED_AFTER_MLS_MIGRATION:
case ClientEvent.CONVERSATION.MLS_MIGRATION_ONGOING_CALL:
case ClientEvent.CONVERSATION.MLS_CONVERSATION_RECOVERED:
case ClientEvent.CONVERSATION.ONE2ONE_MIGRATED_TO_MLS:
case ClientEvent.CONVERSATION.UNABLE_TO_DECRYPT:
case ClientEvent.CONVERSATION.VERIFICATION:
case ClientEvent.CONVERSATION.E2EI_VERIFICATION:
Expand Down
6 changes: 5 additions & 1 deletion src/script/conversation/ConversationSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export function isTeamConversation(conversation: Conversation): boolean {
return conversation.type() === CONVERSATION_TYPE.GLOBAL_TEAM;
}

export function isConnectionRequestConversation(conversation: Conversation): boolean {
return conversation.type() === CONVERSATION_TYPE.CONNECT;
}

interface ProtocolToConversationType {
[ConversationProtocol.PROTEUS]: ProteusConversation;
[ConversationProtocol.MLS]: MLSConversation;
Expand All @@ -78,7 +82,7 @@ const is1to1ConversationWithUser =
}

const isProteusConnectType =
protocol === ConversationProtocol.PROTEUS && conversation.type() === CONVERSATION_TYPE.CONNECT;
protocol === ConversationProtocol.PROTEUS && isConnectionRequestConversation(conversation);

if (!conversation.is1to1() && !isProteusConnectType) {
return false;
Expand Down
13 changes: 13 additions & 0 deletions src/script/conversation/EventBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type VoiceChannelDeactivateEvent = ConversationEvent<

export type AllVerifiedEventData = {type: VerificationMessageType.VERIFIED};
export type AllVerifiedEvent = ConversationEvent<CONVERSATION.VERIFICATION, AllVerifiedEventData>;
export type OneToOneMigratedToMlsEvent = ConversationEvent<CONVERSATION.ONE2ONE_MIGRATED_TO_MLS>;
export type AssetAddEvent = ConversationEvent<
CONVERSATION.ASSET_ADD,
{
Expand Down Expand Up @@ -252,6 +253,7 @@ export type ClientConversationEvent =
| MemberLeaveEvent
| MemberJoinEvent
| OneToOneCreationEvent
| OneToOneMigratedToMlsEvent
| VoiceChannelDeactivateEvent
| FileTypeRestrictedEvent
| CallingTimeoutEvent
Expand Down Expand Up @@ -290,6 +292,17 @@ export const EventBuilder = {
};
},

build1to1MigratedToMLS(conversationEntity: Conversation): OneToOneMigratedToMlsEvent {
return {
...buildQualifiedId(conversationEntity),
time: new Date(conversationEntity.getNextTimestamp()).toISOString(),
type: ClientEvent.CONVERSATION.ONE2ONE_MIGRATED_TO_MLS,
from: conversationEntity.selfUser().id,
data: undefined,
id: createUuid(),
};
},

buildAllVerified(conversationEntity: Conversation): AllVerifiedEvent {
return {
...buildQualifiedId(conversationEntity),
Expand Down
15 changes: 14 additions & 1 deletion src/script/conversation/EventMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {MessageTimerUpdateMessage} from '../entity/message/MessageTimerUpdateMes
import {MissedMessage} from '../entity/message/MissedMessage';
import {MLSConversationRecoveredMessage} from '../entity/message/MLSConversationRecoveredMessage';
import {MLSMigrationFinalisationOngoingCallMessage} from '../entity/message/MLSMigrationFinalisationOngoingCallMessage';
import {OneToOneMigratedToMlsMessage} from '../entity/message/OneToOneMigratedToMlsMessage';
import {PingMessage} from '../entity/message/PingMessage';
import {ProtocolUpdateMessage} from '../entity/message/ProtocolUpdateMessage';
import {ReceiptModeUpdateMessage} from '../entity/message/ReceiptModeUpdateMessage';
Expand Down Expand Up @@ -356,6 +357,11 @@ export class EventMapper {
break;
}

case ClientEvent.CONVERSATION.ONE2ONE_MIGRATED_TO_MLS: {
messageEntity = this._mapEventOneToOneMigratedToMls();
break;
}

case ClientEvent.CONVERSATION.TEAM_MEMBER_LEAVE: {
messageEntity = this._mapEventTeamMemberLeave(event);
break;
Expand Down Expand Up @@ -678,10 +684,17 @@ export class EventMapper {
/**
* Maps JSON data of local MLS conversation recovered event to message entity.
*/
private _mapEventMLSConversationRecovered(): MissedMessage {
private _mapEventMLSConversationRecovered(): MLSConversationRecoveredMessage {
return new MLSConversationRecoveredMessage();
}

/**
* Maps 1:1 conversation migrated to mls event to message entity.
*/
private _mapEventOneToOneMigratedToMls(): OneToOneMigratedToMlsMessage {
return new OneToOneMigratedToMlsMessage();
}

/**
* Maps JSON data of `conversation.knock` message into message entity.
*/
Expand Down
36 changes: 36 additions & 0 deletions src/script/entity/message/OneToOneMigratedToMlsMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {Config} from 'src/script/Config';
import {SystemMessageType} from 'src/script/message/SystemMessageType';
import {replaceLink, t} from 'Util/LocalizerUtil';

import {SystemMessage} from './SystemMessage';

export class OneToOneMigratedToMlsMessage extends SystemMessage {
constructor() {
super();
this.system_message_type = SystemMessageType.ONE2ONE_MIGRATED_TO_MLS;
this.caption = t(
'conversationProtocolUpdatedToMLS',
{},
replaceLink(Config.getConfig().URL.SUPPORT.MLS_LEARN_MORE),
);
}
}
1 change: 1 addition & 0 deletions src/script/event/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export enum CONVERSATION {
VOICE_CHANNEL_ACTIVATE = 'conversation.voice-channel-activate',
VOICE_CHANNEL_DEACTIVATE = 'conversation.voice-channel-deactivate',
E2EI_VERIFICATION = 'conversation.e2ei-verification',
ONE2ONE_MIGRATED_TO_MLS = 'conversation.one2one-migrated-to-mls',
}

export enum USER {
Expand Down
1 change: 1 addition & 0 deletions src/script/event/EventTypeHandling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const EventTypeHandling = {
ClientEvent.CONVERSATION.JOINED_AFTER_MLS_MIGRATION,
ClientEvent.CONVERSATION.MLS_MIGRATION_ONGOING_CALL,
ClientEvent.CONVERSATION.MLS_CONVERSATION_RECOVERED,
ClientEvent.CONVERSATION.ONE2ONE_MIGRATED_TO_MLS,
ClientEvent.CONVERSATION.ONE2ONE_CREATION,
ClientEvent.CONVERSATION.TEAM_MEMBER_LEAVE,
ClientEvent.CONVERSATION.UNABLE_TO_DECRYPT,
Expand Down
1 change: 1 addition & 0 deletions src/script/message/SystemMessageType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ export enum SystemMessageType {
MEMBER_LEAVE = 'leave',
NORMAL = 'normal',
MLS_CONVERSATION_RECOVERED = 'mls-conversation-recovered',
ONE2ONE_MIGRATED_TO_MLS = 'one2one-migrated-to-mls',
E2EI_VERIFIED = 'e2ei-verified',
}

0 comments on commit 6d34620

Please sign in to comment.