Skip to content

Commit

Permalink
feat: start migration of proteus conversations [FS-1888] (#15198)
Browse files Browse the repository at this point in the history
* feat: base for initialisation and finalisation methods

* feat: check if migration time has arrived after mls is supported

* refactor: improve init migration api

* refactor: rename func

* refactor: move initialisation and finalisation to separate modules

* runfix: start migration flow in background after app was initialised

* feat: filter unestablished mls conversations out

* feat: filter out only mls conversations that are unestablished

* refactor: move mls migration logger to separate file

* feat: update conversation locally

* chore: remove migration init code (follow-up pr)

* refactor: resolve core and api client in module

* feat: read feature config from teamstate

* chore: update core

* runfix: check fresh migration config when timer elapses

* chore: update comment

* refactor: use reduce for grouping conversations by protocol

* refactor: simplify removal key check

* feat: update core with new migration config types

* refactor: improve types

* feat: initialise migration of proteus conversations

* feat: create mls group after switching to mixed and add other clients

* runfix: send messages with mls if conversation is actually mls (not if group id exists)

* chore: bump core

* refactor: don't replace conversation's reference

* feat: insert system message after conversation protocol update

* refactor: improve reaction to protocol update event

* refactor: move protocol update logic to conversation repository

* feat: save conversation state to db after updating protocol

* feat: update conversation protocol-related fields after protocol was updated

* refactor: move adding users of conversation to separate module

* refactor: move establishing group for mixed conversation to separate module

* chore: update comments

* runfix: don't try to to add users to mls group if mixed conv is empty

* test: adding all conversation members to mls group

* test: try establishing mls group for mixed conversation

* chore: remove comment

* test: conversation repo updateConversationProtocol

* test: initialise migration of proteus conversations

* refactor: CR suggestions
  • Loading branch information
PatrykBuniX committed May 30, 2023
1 parent 1882115 commit 06859f8
Show file tree
Hide file tree
Showing 25 changed files with 975 additions and 11 deletions.
3 changes: 3 additions & 0 deletions src/__mocks__/@wireapp/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export class Account extends EventEmitter {

service = {
mls: {
addUsersToExistingConversation: jest.fn(),
conversationExists: jest.fn(),
wipeConversation: jest.fn(),
registerConversation: jest.fn(),
joinConferenceSubconversation: jest.fn(),
getGroupIdFromConversationId: jest.fn(),
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@
"conversationYouAccusative": "you",
"conversationYouDative": "you",
"conversationYouNominative": "you",
"conversationProtocolUpdatedToMixed": "Conversation has started migration to MLS protocol.",
"conversationProtocolUpdatedToMLS": "Conversation has finalised migration to MLS protocol.",
"conversationYouRemovedMissingLegalHoldConsent": "[bold]You[/bold] were removed from this conversation because legal hold has been activated. [link]Learn more[/link]",
"conversationsAllArchived": "Everything archived",
"conversationsConnectionRequestMany": "{{number}} people waiting",
Expand Down
5 changes: 5 additions & 0 deletions src/script/components/MessagesList/Message/MessageWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {LegalHoldMessage} from './LegalHoldMessage';
import {MemberMessage} from './MemberMessage';
import {MissedMessage} from './MissedMessage';
import {PingMessage} from './PingMessage';
import {ProtocolUpdateMessage} from './ProtocolUpdateMessage';
import {SystemMessage} from './SystemMessage';
import {VerificationMessage} from './VerificationMessage';

Expand Down Expand Up @@ -269,8 +270,12 @@ export const MessageWrapper: React.FC<MessageParams & {hasMarker: boolean; isMes
if (message.isFileTypeRestricted()) {
return <FileTypeRestrictedMessage message={message} />;
}
if (message.isProtocolUpdate()) {
return <ProtocolUpdateMessage message={message} />;
}
if (message.isMissed()) {
return <MissedMessage />;
}

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Wire
* Copyright (C) 2021 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 React from 'react';

import {Icon} from 'Components/Icon';
import {ProtocolUpdateMessage as ProtocolUpdateMessageEntity} from 'src/script/entity/message/ProtocolUpdateMessage';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';

import {MessageTime} from './MessageTime';

export interface ProtocolUpdateMessageProps {
message: ProtocolUpdateMessageEntity;
}

export const ProtocolUpdateMessage: React.FC<ProtocolUpdateMessageProps> = ({message}) => {
const {caption, timestamp} = useKoSubscribableChildren(message, ['caption', 'timestamp']);

return (
<div className="message-header" data-uie-name="element-message-protocol-update">
<div className="message-header-icon message-header-icon--svg text-foreground">
<Icon.Info />
</div>
<div className="message-header-label" data-uie-name="element-message-protocol-update-text">
<p className="message-header-label__multiline">
<span className="ellipsis">
<strong>{caption}</strong>
</span>
</p>
</div>
<div className="message-body-actions">
<MessageTime timestamp={timestamp} data-uie-uid={message.id} data-uie-name="item-message-call-timestamp" />
</div>
</div>
);
};
102 changes: 101 additions & 1 deletion src/script/conversation/ConversationRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ import {faker} from '@faker-js/faker';
import {ClientClassification} from '@wireapp/api-client/lib/client/';
import {ConnectionStatus} from '@wireapp/api-client/lib/connection/';
import {
Conversation as BackendConversation,
CONVERSATION_ACCESS,
CONVERSATION_LEGACY_ACCESS_ROLE,
CONVERSATION_TYPE,
RemoteConversations,
} from '@wireapp/api-client/lib/conversation/';
import {RECEIPT_MODE} from '@wireapp/api-client/lib/conversation/data';
import {ConversationProtocol} from '@wireapp/api-client/lib/conversation/NewConversation';
import {ConversationCreateEvent, ConversationMemberJoinEvent, CONVERSATION_EVENT} from '@wireapp/api-client/lib/event/';
import {
ConversationCreateEvent,
ConversationMemberJoinEvent,
ConversationProtocolUpdateEvent,
CONVERSATION_EVENT,
} from '@wireapp/api-client/lib/event/';
import {QualifiedId} from '@wireapp/api-client/lib/user';
import {amplify} from 'amplify';
import {StatusCodes as HTTP_STATUS} from 'http-status-codes';
Expand Down Expand Up @@ -86,6 +92,53 @@ const _generateConversation = (
return conversation;
};

const generateConversationBackendResponse = (protocol: ConversationProtocol, cipherSuite = 1, epoch = 1) => {
const conversationBackendResponse = {
access: ['invite', 'code'],
access_role: ['team_member', 'non_team_member', 'guest', 'service'],
cipher_suite: cipherSuite,
creator: '2695c0ea-68e3-4e1b-8a18-04c4ef24a3b0',
epoch: protocol === ConversationProtocol.PROTEUS ? -1 : epoch,
epoch_timestamp: '2023-05-24T06:54:46.112Z',
group_id: protocol === ConversationProtocol.PROTEUS ? undefined : 'W6NsZUcqwg/jyX4WeKfXgCEdjOxnUN6jsqRebJFNtDU=',
id: 'c5ac85a8-90a5-4973-ae34-e474a95337a0',
last_event: '0.0',
last_event_time: '1970-01-01T00:00:00.000Z',
members: {
others: [],
self: {
hidden_ref: null,
id: '2695c0ea-68e3-4e1b-8a18-04c4ef24a3b0',
otr_archived: false,
otr_archived_ref: null,
otr_muted_ref: null,
otr_muted_status: null,
qualified_id: {
domain: 'anta.wire.link',
id: '2695c0ea-68e3-4e1b-8a18-04c4ef24a3b0',
},
service: null,
status: 0,
status_ref: '0.0',
status_time: '1970-01-01T00:00:00.000Z',
},
},
message_timer: 0,
name: 'conference',
protocol,
qualified_id: {
domain: 'anta.wire.link',
id: 'c5ac85a8-90a5-4973-ae34-e474a95337a0',
},
receipt_mode: 1,
team: '7491ae3b-b5e3-4822-b158-acd855fe5e95',
type: 0,
failed_to_add: [],
} as BackendConversation;

return conversationBackendResponse;
};

describe('ConversationRepository', () => {
const testFactory = new TestFactory();

Expand Down Expand Up @@ -1526,4 +1579,51 @@ describe('ConversationRepository', () => {
expect(conversationRepo['refreshAllConversationsUnavailableParticipants']).toHaveBeenCalled();
});
});

describe('updateConversationProtocol', () => {
it('should update the protocol-related fields after protocol was updated to mixed and inject event', async () => {
const conversation = _generateConversation();
const conversationRepository = await testFactory.exposeConversationActors();

const mockedProtocolUpdateEventResponse = {
data: {
protocol: ConversationProtocol.MIXED,
},
qualified_conversation: {
domain: 'anta.wire.link',
id: 'fb1c0e0f-60a9-4a6c-9644-041260e7aac9',
},
time: '2020-10-13T14:00:00.000Z',
type: CONVERSATION_EVENT.PROTOCOL_UPDATE,
} as ConversationProtocolUpdateEvent;

jest
.spyOn(conversationRepository['conversationService'], 'updateConversationProtocol')
.mockResolvedValueOnce(mockedProtocolUpdateEventResponse);

const newProtocol = ConversationProtocol.MIXED;
const newCipherSuite = 1;
const newEpoch = 2;
const mockedConversationResponse = generateConversationBackendResponse(newProtocol, newCipherSuite, newEpoch);
jest
.spyOn(conversationRepository['conversationService'], 'getConversationById')
.mockResolvedValueOnce(mockedConversationResponse);

jest.spyOn(conversationRepository['eventRepository'], 'injectEvent').mockResolvedValueOnce(undefined);

const updatedConversation = await conversationRepository.updateConversationProtocol(
conversation,
ConversationProtocol.MIXED,
);

expect(conversationRepository['eventRepository'].injectEvent).toHaveBeenCalledWith(
mockedProtocolUpdateEventResponse,
EventRepository.SOURCE.BACKEND_RESPONSE,
);

expect(updatedConversation.protocol).toEqual(ConversationProtocol.MIXED);
expect(updatedConversation.cipherSuite).toEqual(newCipherSuite);
expect(updatedConversation.epoch).toEqual(newEpoch);
});
});
});
68 changes: 67 additions & 1 deletion src/script/conversation/ConversationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
ConversationRenameEvent,
ConversationTypingEvent,
CONVERSATION_EVENT,
ConversationProtocolUpdateEvent,
} from '@wireapp/api-client/lib/event';
import {BackendErrorLabel} from '@wireapp/api-client/lib/http/';
import type {BackendError} from '@wireapp/api-client/lib/http/';
Expand Down Expand Up @@ -1334,7 +1335,7 @@ export class ConversationRepository {
* @returns Mapped conversation/s
*/
mapConversations(
payload: BackendConversation[],
payload: (BackendConversation | ConversationDatabaseData)[],
initialTimestamp = this.getLatestEventTimestamp(true),
): Conversation[] {
const entities = ConversationMapper.mapConversations(payload as ConversationDatabaseData[], initialTimestamp);
Expand Down Expand Up @@ -1709,6 +1710,56 @@ export class ConversationRepository {
return undefined;
}

/**
* 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)
* If protocol was updated successfully, conversation protocol update system message will be injected
*
* @param conversationId id of the conversation
* @param protocol new conversation protocol
* @returns Resolves with updated conversation entity
*/
public async updateConversationProtocol(
conversation: Conversation,
protocol: ConversationProtocol.MIXED | ConversationProtocol.MLS,
): Promise<Conversation> {
const protocolUpdateEventResponse = await this.conversationService.updateConversationProtocol(
conversation.qualifiedId,
protocol,
);
if (protocolUpdateEventResponse) {
await this.eventRepository.injectEvent(protocolUpdateEventResponse, EventRepository.SOURCE.BACKEND_RESPONSE);
}

//even if protocol was already updated (no response), we need to refetch the conversation
return this.refreshConversationProtocolProperties(conversation);
}

/**
* Refresh conversation protocol properties
* Will refetch the conversation to get all new protocol-related fields (groupId, ciphersuite, epoch and new protocol)
* Will update the conversation entity in memory and in the local database
*
* @param conversationId id of the conversation
* @returns Resolves with updated conversation entity
*/
private async refreshConversationProtocolProperties(conversation: Conversation) {
//refetch the conversation to get all new fields (groupId, ciphersuite, epoch and new protocol)
const remoteConversationData = await this.conversationService.getConversationById(conversation.qualifiedId);

//update fields that came after protocol update
const {cipher_suite: cipherSuite, epoch, group_id: newGroupId, protocol: newProtocol} = remoteConversationData;
const updatedConversation = ConversationMapper.updateProperties(conversation, {
cipherSuite,
epoch,
groupId: newGroupId,
protocol: newProtocol,
});

await this.saveConversationStateInDb(updatedConversation);
return updatedConversation;
}

/**
* Set the global message timer
*/
Expand Down Expand Up @@ -2293,6 +2344,9 @@ export class ConversationRepository {
case CONVERSATION_EVENT.TYPING:
return this.onTyping(conversationEntity, eventJson);

case CONVERSATION_EVENT.PROTOCOL_UPDATE:
return this.onProtocolUpdate(conversationEntity, eventJson);

case CONVERSATION_EVENT.RENAME:
return this.onRename(conversationEntity, eventJson, eventSource === EventRepository.SOURCE.WEB_SOCKET);

Expand Down Expand Up @@ -2973,6 +3027,18 @@ export class ConversationRepository {
return {conversationEntity, messageEntity};
}

/**
* Conversation protocol was updated.
*
* @param conversation Conversation that has updated protocol
* @param eventJson JSON data of 'conversation.protocol-update' event
* @returns Resolves when the event was handled
*/
private async onProtocolUpdate(conversation: Conversation, eventJson: ConversationProtocolUpdateEvent) {
const updatedConversation = await this.refreshConversationProtocolProperties(conversation);
return this.addEventToConversation(updatedConversation, eventJson);
}

/**
* A user started or stopped typing in a conversation.
*
Expand Down
18 changes: 18 additions & 0 deletions src/script/conversation/ConversationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ import type {
ConversationCode,
CONVERSATION_ACCESS,
RemoteConversations,
ConversationProtocol,
} from '@wireapp/api-client/lib/conversation';
import type {
ConversationJoinData,
ConversationMemberUpdateData,
ConversationOtherMemberUpdateData,
ConversationReceiptModeUpdateData,
} from '@wireapp/api-client/lib/conversation/data';
import {ConversationProtocolUpdateEvent} from '@wireapp/api-client/lib/event';
import type {
ConversationCodeDeleteEvent,
ConversationCodeUpdateEvent,
Expand Down Expand Up @@ -110,6 +112,22 @@ export class ConversationService {
});
}

/**
* Update the conversation protocol.
*
* @see https://staging-nginz-https.zinfra.io/swagger-ui/#!/conversations/updateConversation
*
* @param conversationId ID of conversation to rename
* @param protocol new protocol of the conversation
* @returns Resolves with the server response
*/
updateConversationProtocol(
conversationId: QualifiedId,
protocol: ConversationProtocol.MIXED | ConversationProtocol.MLS,
): Promise<ConversationProtocolUpdateEvent | null> {
return this.apiClient.api.conversation.putConversationProtocol(conversationId, protocol);
}

/**
* Update the conversation message timer value.
*
Expand Down
Loading

0 comments on commit 06859f8

Please sign in to comment.