From 6d3462076ed0681d0df4103420180b085cdbfa66 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:26:41 +0100 Subject: [PATCH] feat: one2one migrated to mls system message [WPB-6195] (#16560) (#16590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Thomas Belin --- .../Message/SystemMessage/SystemMessage.tsx | 5 +++ .../ConversationRepository.test.ts | 2 + .../conversation/ConversationRepository.ts | 43 +++++++++++++++---- .../conversation/ConversationSelectors.ts | 6 ++- src/script/conversation/EventBuilder.ts | 13 ++++++ src/script/conversation/EventMapper.ts | 15 ++++++- .../message/OneToOneMigratedToMlsMessage.ts | 36 ++++++++++++++++ src/script/event/Client.ts | 1 + src/script/event/EventTypeHandling.ts | 1 + src/script/message/SystemMessageType.ts | 1 + 10 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 src/script/entity/message/OneToOneMigratedToMlsMessage.ts diff --git a/src/script/components/MessagesList/Message/SystemMessage/SystemMessage.tsx b/src/script/components/MessagesList/Message/SystemMessage/SystemMessage.tsx index 565f25ba473..a2b3699e3ad 100644 --- a/src/script/components/MessagesList/Message/SystemMessage/SystemMessage.tsx +++ b/src/script/components/MessagesList/Message/SystemMessage/SystemMessage.tsx @@ -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'; @@ -74,6 +75,10 @@ export const SystemMessage: React.FC = ({message}) => { return } />; } + if (message instanceof OneToOneMigratedToMlsMessage) { + return } />; + } + if (message instanceof MLSMigrationFinalisationOngoingCallMessage) { return } />; } diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts index a62ffb4a69b..8b831a85cb2 100644 --- a/src/script/conversation/ConversationRepository.test.ts +++ b/src/script/conversation/ConversationRepository.test.ts @@ -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); @@ -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]), ); diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts index 9bb84fac350..051d5844794 100644 --- a/src/script/conversation/ConversationRepository.ts +++ b/src/script/conversation/ConversationRepository.ts @@ -89,6 +89,7 @@ import { MLSConversation, isProteus1to1ConversationWithUser, ProteusConversation, + isConnectionRequestConversation, } from './ConversationSelectors'; import {ConversationService} from './ConversationService'; import {ConversationState} from './ConversationState'; @@ -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}; } @@ -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}`); @@ -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); @@ -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, ); @@ -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); @@ -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(); @@ -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) @@ -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: diff --git a/src/script/conversation/ConversationSelectors.ts b/src/script/conversation/ConversationSelectors.ts index 1dcac39bf0c..8c359758291 100644 --- a/src/script/conversation/ConversationSelectors.ts +++ b/src/script/conversation/ConversationSelectors.ts @@ -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; @@ -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; diff --git a/src/script/conversation/EventBuilder.ts b/src/script/conversation/EventBuilder.ts index b6d80ede2c8..902acd68a1a 100644 --- a/src/script/conversation/EventBuilder.ts +++ b/src/script/conversation/EventBuilder.ts @@ -78,6 +78,7 @@ export type VoiceChannelDeactivateEvent = ConversationEvent< export type AllVerifiedEventData = {type: VerificationMessageType.VERIFIED}; export type AllVerifiedEvent = ConversationEvent; +export type OneToOneMigratedToMlsEvent = ConversationEvent; export type AssetAddEvent = ConversationEvent< CONVERSATION.ASSET_ADD, { @@ -252,6 +253,7 @@ export type ClientConversationEvent = | MemberLeaveEvent | MemberJoinEvent | OneToOneCreationEvent + | OneToOneMigratedToMlsEvent | VoiceChannelDeactivateEvent | FileTypeRestrictedEvent | CallingTimeoutEvent @@ -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), diff --git a/src/script/conversation/EventMapper.ts b/src/script/conversation/EventMapper.ts index e68d41bd45b..d9d8bf9f2ae 100644 --- a/src/script/conversation/EventMapper.ts +++ b/src/script/conversation/EventMapper.ts @@ -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'; @@ -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; @@ -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. */ diff --git a/src/script/entity/message/OneToOneMigratedToMlsMessage.ts b/src/script/entity/message/OneToOneMigratedToMlsMessage.ts new file mode 100644 index 00000000000..173b94cb20f --- /dev/null +++ b/src/script/entity/message/OneToOneMigratedToMlsMessage.ts @@ -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), + ); + } +} diff --git a/src/script/event/Client.ts b/src/script/event/Client.ts index f83c2b3a96f..c6268e724f3 100644 --- a/src/script/event/Client.ts +++ b/src/script/event/Client.ts @@ -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 { diff --git a/src/script/event/EventTypeHandling.ts b/src/script/event/EventTypeHandling.ts index b801a5c9b47..b8c1c6663ee 100644 --- a/src/script/event/EventTypeHandling.ts +++ b/src/script/event/EventTypeHandling.ts @@ -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, diff --git a/src/script/message/SystemMessageType.ts b/src/script/message/SystemMessageType.ts index c6308b93e47..2f9650e42c3 100644 --- a/src/script/message/SystemMessageType.ts +++ b/src/script/message/SystemMessageType.ts @@ -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', }