From 940a57abf4482229d289675802186e8a1e96fa14 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 20 Jul 2023 14:13:54 +0200
Subject: [PATCH 01/70] feat: self user supported protocols
---
src/script/main/app.ts | 1 +
.../evaluateSelfSupportedProtocols.test.ts | 212 ++++++++++++++++++
.../evaluateSelfSupportedProtocols.ts | 144 ++++++++++++
.../evaluateSelfSupportedProtocols/index.ts | 20 ++
src/script/mls/supportedProtocols/index.ts | 20 ++
.../supportedProtocols.test.ts | 111 +++++++++
.../supportedProtocols/supportedProtocols.ts | 88 ++++++++
.../wasClientActiveWithinLast4Weeks/index.ts | 20 ++
.../wasClientActiveWithinLast4Weeks.test.ts | 45 ++++
.../wasClientActiveWithinLast4Weeks.ts | 32 +++
10 files changed, 693 insertions(+)
create mode 100644 src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts
create mode 100644 src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts
create mode 100644 src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/index.ts
create mode 100644 src/script/mls/supportedProtocols/index.ts
create mode 100644 src/script/mls/supportedProtocols/supportedProtocols.test.ts
create mode 100644 src/script/mls/supportedProtocols/supportedProtocols.ts
create mode 100644 src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/index.ts
create mode 100644 src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.test.ts
create mode 100644 src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.ts
diff --git a/src/script/main/app.ts b/src/script/main/app.ts
index 398dff87351..7166172c2cf 100644
--- a/src/script/main/app.ts
+++ b/src/script/main/app.ts
@@ -81,6 +81,7 @@ import {IntegrationService} from '../integration/IntegrationService';
import {startNewVersionPolling} from '../lifecycle/newVersionHandler';
import {MediaRepository} from '../media/MediaRepository';
import {initMLSCallbacks, initMLSConversations, registerUninitializedSelfAndTeamConversations} from '../mls';
+import {initialisePeriodicSelfSupportedProtocolsCheck} from '../mls/supportedProtocols';
import {NotificationRepository} from '../notification/NotificationRepository';
import {PreferenceNotificationRepository} from '../notification/PreferenceNotificationRepository';
import {PermissionRepository} from '../permission/PermissionRepository';
diff --git a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts
new file mode 100644
index 00000000000..94419d97351
--- /dev/null
+++ b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts
@@ -0,0 +1,212 @@
+/*
+ * 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 {RegisteredClient} from '@wireapp/api-client/lib/client';
+import {ConversationProtocol} from '@wireapp/api-client/lib/conversation';
+import {FeatureList, FeatureStatus} from '@wireapp/api-client/lib/team';
+
+import {TestFactory} from 'test/helper/TestFactory';
+import {TIME_IN_MILLIS} from 'Util/TimeUtil';
+
+import {evaluateSelfSupportedProtocols} from './evaluateSelfSupportedProtocols';
+
+import * as mlsSupport from '../../isMLSSupportedByEnvironment';
+import {MLSMigrationStatus} from '../../MLSMigration/migrationStatus';
+
+const testFactory = new TestFactory();
+
+jest.spyOn(mlsSupport, 'isMLSSupportedByEnvironment').mockResolvedValue(true);
+
+const generateMLSFeaturesConfig = (migrationStatus: MLSMigrationStatus, supportedProtocols: ConversationProtocol[]) => {
+ const now = Date.now();
+
+ switch (migrationStatus) {
+ case MLSMigrationStatus.DISABLED:
+ return {
+ mls: {
+ status: FeatureStatus.ENABLED,
+ config: {supportedProtocols},
+ },
+ mlsMigration: {status: FeatureStatus.DISABLED, config: {}},
+ };
+ case MLSMigrationStatus.NOT_STARTED:
+ return {
+ mls: {
+ status: FeatureStatus.ENABLED,
+ config: {supportedProtocols},
+ },
+ mlsMigration: {
+ status: FeatureStatus.ENABLED,
+ config: {
+ startTime: new Date(now + 1 * TIME_IN_MILLIS.DAY).toISOString(),
+ finaliseRegardlessAfter: new Date(now + 2 * TIME_IN_MILLIS.DAY).toISOString(),
+ },
+ },
+ };
+ case MLSMigrationStatus.ONGOING:
+ return {
+ mls: {
+ status: FeatureStatus.ENABLED,
+ config: {supportedProtocols},
+ },
+ mlsMigration: {
+ status: FeatureStatus.ENABLED,
+ config: {
+ startTime: new Date(now - 1 * TIME_IN_MILLIS.DAY).toISOString(),
+ finaliseRegardlessAfter: new Date(now + 1 * TIME_IN_MILLIS.DAY).toISOString(),
+ },
+ },
+ };
+ case MLSMigrationStatus.FINALISED:
+ return {
+ mls: {
+ status: FeatureStatus.ENABLED,
+ config: {supportedProtocols},
+ },
+ mlsMigration: {
+ status: FeatureStatus.ENABLED,
+ config: {
+ startTime: new Date(now - 2 * TIME_IN_MILLIS.DAY).toISOString(),
+ finaliseRegardlessAfter: new Date(now - 1 * TIME_IN_MILLIS.DAY).toISOString(),
+ },
+ },
+ };
+ }
+};
+
+const createMockClientResponse = (doesSupportMLS = false, wasActiveWithinLast4Weeks = false) => {
+ return {
+ mls_public_keys: doesSupportMLS ? {ed25519: 'key'} : undefined,
+ last_active: wasActiveWithinLast4Weeks
+ ? new Date().toISOString()
+ : new Date(Date.now() - 5 * 7 * 24 * 60 * 60 * 1000).toISOString(),
+ } as unknown as RegisteredClient;
+};
+
+const generateListOfSelfClients = ({allActiveClientsMLSCapable}: {allActiveClientsMLSCapable: boolean}) => {
+ const clients: RegisteredClient[] = [];
+
+ new Array(4).fill(0).forEach(() => clients.push(createMockClientResponse(true, true)));
+ if (!allActiveClientsMLSCapable) {
+ new Array(2).fill(0).forEach(() => clients.push(createMockClientResponse(false, true)));
+ }
+
+ return clients;
+};
+
+const testScenarios = [
+ [
+ //with given config
+ generateMLSFeaturesConfig(MLSMigrationStatus.DISABLED, [ConversationProtocol.PROTEUS]),
+
+ //we expect the following result based on whether all active clients are MLS capable or not
+ {
+ allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ },
+ ],
+ [
+ generateMLSFeaturesConfig(MLSMigrationStatus.DISABLED, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ {
+ allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ },
+ ],
+ [
+ generateMLSFeaturesConfig(MLSMigrationStatus.DISABLED, [ConversationProtocol.MLS]),
+ {
+ allActiveClientsMLSCapable: new Set([ConversationProtocol.MLS]),
+ someActiveClientsNotMLSCapable: new Set([ConversationProtocol.MLS]),
+ },
+ ],
+ [
+ generateMLSFeaturesConfig(MLSMigrationStatus.NOT_STARTED, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ {
+ allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ },
+ ],
+ [
+ generateMLSFeaturesConfig(MLSMigrationStatus.NOT_STARTED, [ConversationProtocol.MLS]),
+ {
+ allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ },
+ ],
+ [
+ generateMLSFeaturesConfig(MLSMigrationStatus.ONGOING, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ {
+ allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ },
+ ],
+ [
+ generateMLSFeaturesConfig(MLSMigrationStatus.ONGOING, [ConversationProtocol.MLS]),
+ {
+ allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ },
+ ],
+ [
+ generateMLSFeaturesConfig(MLSMigrationStatus.FINALISED, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ {
+ allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ },
+ ],
+ [
+ generateMLSFeaturesConfig(MLSMigrationStatus.FINALISED, [ConversationProtocol.MLS]),
+ {
+ allActiveClientsMLSCapable: new Set([ConversationProtocol.MLS]),
+ someActiveClientsNotMLSCapable: new Set([ConversationProtocol.MLS]),
+ },
+ ],
+] as const;
+
+describe('evaluateSelfSupportedProtocols', () => {
+ describe.each([{allActiveClientsMLSCapable: true}, {allActiveClientsMLSCapable: false}])(
+ '%o',
+ ({allActiveClientsMLSCapable}) => {
+ const selfClients = generateListOfSelfClients({allActiveClientsMLSCapable});
+
+ it.each(testScenarios)('evaluates self supported protocols', async ({mls, mlsMigration}, expected) => {
+ const teamRepository = await testFactory.exposeTeamActors();
+ const userRepository = await testFactory.exposeUserActors();
+
+ jest.spyOn(userRepository, 'getAllSelfClients').mockResolvedValueOnce(selfClients);
+
+ const teamFeatureList = {
+ mlsMigration,
+ mls,
+ } as unknown as FeatureList;
+
+ jest.spyOn(teamRepository['teamState'], 'teamFeatures').mockReturnValue(teamFeatureList);
+
+ const supportedProtocols = await evaluateSelfSupportedProtocols({
+ teamRepository,
+ userRepository,
+ });
+
+ expect(supportedProtocols).toEqual(
+ allActiveClientsMLSCapable ? expected.allActiveClientsMLSCapable : expected.someActiveClientsNotMLSCapable,
+ );
+ });
+ },
+ );
+});
diff --git a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts
new file mode 100644
index 00000000000..028ea4c02b9
--- /dev/null
+++ b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts
@@ -0,0 +1,144 @@
+/*
+ * 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 {RegisteredClient} from '@wireapp/api-client/lib/client';
+import {ConversationProtocol} from '@wireapp/api-client/lib/conversation';
+
+import {TeamRepository} from 'src/script/team/TeamRepository';
+import {UserRepository} from 'src/script/user/UserRepository';
+
+import {isMLSSupportedByEnvironment} from '../../isMLSSupportedByEnvironment';
+import {MLSMigrationStatus} from '../../MLSMigration/migrationStatus';
+import {wasClientActiveWithinLast4Weeks} from '../wasClientActiveWithinLast4Weeks';
+
+export const evaluateSelfSupportedProtocols = async ({
+ teamRepository,
+ userRepository,
+}: {
+ teamRepository: TeamRepository;
+ userRepository: UserRepository;
+}): Promise> => {
+ const supportedProtocols = new Set();
+
+ const teamSupportedProtocols = teamRepository.getTeamSupportedProtocols();
+ const mlsMigrationStatus = teamRepository.getTeamMLSMigrationStatus();
+
+ const selfClients = await userRepository.getAllSelfClients();
+
+ const isProteusProtocolSupported = await isProteusSupported({teamSupportedProtocols, mlsMigrationStatus});
+ if (isProteusProtocolSupported) {
+ supportedProtocols.add(ConversationProtocol.PROTEUS);
+ }
+
+ const mlsCheckDependencies = {
+ teamSupportedProtocols,
+ selfClients,
+ mlsMigrationStatus,
+ };
+
+ const isMLSProtocolSupported = await isMLSSupported(mlsCheckDependencies);
+
+ const isMLSForced = await isMLSForcedWithoutMigration(mlsCheckDependencies);
+
+ if (isMLSProtocolSupported || isMLSForced) {
+ supportedProtocols.add(ConversationProtocol.MLS);
+ }
+
+ return supportedProtocols;
+};
+
+/*
+ MLS is supported if:
+ - MLS is in the list of supported protocols
+ - All active clients support MLS, or MLS migration is finalised
+*/
+const isMLSSupported = async ({
+ teamSupportedProtocols,
+ selfClients,
+ mlsMigrationStatus,
+}: {
+ teamSupportedProtocols: Set;
+ selfClients: RegisteredClient[];
+ mlsMigrationStatus: MLSMigrationStatus;
+}): Promise => {
+ const isMLSSupportedByEnv = await isMLSSupportedByEnvironment();
+
+ if (!isMLSSupportedByEnv) {
+ return false;
+ }
+
+ const isMLSSupportedByTeam = teamSupportedProtocols.has(ConversationProtocol.MLS);
+ const doActiveClientsSupportMLS = await haveAllActiveClientsRegisteredMLSDevice(selfClients);
+ return isMLSSupportedByTeam && (doActiveClientsSupportMLS || mlsMigrationStatus === MLSMigrationStatus.FINALISED);
+};
+
+/*
+ MLS is forced if:
+ - only MLS is in the list of supported protocols
+ - MLS migration is disabled
+ - There are still some active clients that do not support MLS
+ It means that team admin wants to force MLS and drop proteus support, even though not all active clients support MLS
+*/
+const isMLSForcedWithoutMigration = async ({
+ teamSupportedProtocols,
+ selfClients,
+ mlsMigrationStatus,
+}: {
+ teamSupportedProtocols: Set;
+ selfClients: RegisteredClient[];
+ mlsMigrationStatus: MLSMigrationStatus;
+}): Promise => {
+ const isMLSSupportedByEnv = await isMLSSupportedByEnvironment();
+
+ if (!isMLSSupportedByEnv) {
+ return false;
+ }
+
+ const isMLSSupportedByTeam = teamSupportedProtocols.has(ConversationProtocol.MLS);
+ const isProteusSupportedByTeam = teamSupportedProtocols.has(ConversationProtocol.PROTEUS);
+ const doActiveClientsSupportMLS = await haveAllActiveClientsRegisteredMLSDevice(selfClients);
+ const isMigrationDisabled = mlsMigrationStatus === MLSMigrationStatus.DISABLED;
+
+ return !doActiveClientsSupportMLS && isMLSSupportedByTeam && !isProteusSupportedByTeam && isMigrationDisabled;
+};
+
+/*
+ Proteus is supported if:
+ - Proteus is in the list of supported protocols
+ - MLS migration is enabled but not finalised
+*/
+const isProteusSupported = async ({
+ teamSupportedProtocols,
+ mlsMigrationStatus,
+}: {
+ teamSupportedProtocols: Set;
+ mlsMigrationStatus: MLSMigrationStatus;
+}): Promise => {
+ const isProteusSupportedByTeam = teamSupportedProtocols.has(ConversationProtocol.PROTEUS);
+ return (
+ isProteusSupportedByTeam ||
+ [MLSMigrationStatus.NOT_STARTED, MLSMigrationStatus.ONGOING].includes(mlsMigrationStatus)
+ );
+};
+
+const haveAllActiveClientsRegisteredMLSDevice = async (selfClients: RegisteredClient[]): Promise => {
+ //we consider client active if it was active within last 4 weeks
+ const activeClients = selfClients.filter(wasClientActiveWithinLast4Weeks);
+ return activeClients.every(client => !!client.mls_public_keys);
+};
diff --git a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/index.ts b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/index.ts
new file mode 100644
index 00000000000..d106ff936e5
--- /dev/null
+++ b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/index.ts
@@ -0,0 +1,20 @@
+/*
+ * 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/.
+ *
+ */
+
+export * from './evaluateSelfSupportedProtocols';
diff --git a/src/script/mls/supportedProtocols/index.ts b/src/script/mls/supportedProtocols/index.ts
new file mode 100644
index 00000000000..f6b2b21e58c
--- /dev/null
+++ b/src/script/mls/supportedProtocols/index.ts
@@ -0,0 +1,20 @@
+/*
+ * 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/.
+ *
+ */
+
+export * from './supportedProtocols';
diff --git a/src/script/mls/supportedProtocols/supportedProtocols.test.ts b/src/script/mls/supportedProtocols/supportedProtocols.test.ts
new file mode 100644
index 00000000000..8757bd1af3d
--- /dev/null
+++ b/src/script/mls/supportedProtocols/supportedProtocols.test.ts
@@ -0,0 +1,111 @@
+/*
+ * 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 {ConversationProtocol} from '@wireapp/api-client/lib/conversation';
+import {act} from 'react-dom/test-utils';
+
+import {TestFactory} from 'test/helper/TestFactory';
+
+import * as supportedProtocols from './evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols';
+import {initialisePeriodicSelfSupportedProtocolsCheck} from './supportedProtocols';
+
+const testFactory = new TestFactory();
+
+describe('supportedProtocols', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it.each([
+ [[ConversationProtocol.PROTEUS], [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]],
+ [[ConversationProtocol.PROTEUS, ConversationProtocol.MLS], [ConversationProtocol.PROTEUS]],
+ [[ConversationProtocol.PROTEUS], [ConversationProtocol.MLS]],
+ ])('Updates the list of supported protocols', async (initialProtocols, evaluatedProtocols) => {
+ const userRepository = await testFactory.exposeUserActors();
+ const teamRepository = await testFactory.exposeTeamActors();
+
+ const selfUser = userRepository['userState'].self();
+
+ selfUser.supportedProtocols(initialProtocols);
+
+ //this funciton is tested standalone in evaluateSelfSupportedProtocols.test.ts
+ jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(new Set(evaluatedProtocols));
+ jest.spyOn(userRepository, 'changeSupportedProtocols');
+
+ await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
+
+ expect(userRepository.changeSupportedProtocols).toHaveBeenCalledWith(evaluatedProtocols);
+ expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols);
+ });
+
+ it("Does not update supported protocols if they didn't change", async () => {
+ const userRepository = await testFactory.exposeUserActors();
+ const teamRepository = await testFactory.exposeTeamActors();
+
+ const selfUser = userRepository['userState'].self();
+
+ const initialProtocols = [ConversationProtocol.PROTEUS];
+ selfUser.supportedProtocols(initialProtocols);
+
+ const evaluatedProtocols = [ConversationProtocol.PROTEUS];
+
+ //this funciton is tested standalone in evaluateSelfSupportedProtocols.test.ts
+ jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(new Set(evaluatedProtocols));
+ jest.spyOn(userRepository, 'changeSupportedProtocols');
+
+ await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
+ expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols);
+ expect(userRepository.changeSupportedProtocols).not.toHaveBeenCalled();
+ });
+
+ it('Re-evaluates supported protocols every 24h', async () => {
+ const userRepository = await testFactory.exposeUserActors();
+ const teamRepository = await testFactory.exposeTeamActors();
+ const selfUser = userRepository['userState'].self();
+
+ const initialProtocols = [ConversationProtocol.PROTEUS];
+ selfUser.supportedProtocols(initialProtocols);
+
+ const evaluatedProtocols = [ConversationProtocol.PROTEUS];
+
+ //this funciton is tested standalone in evaluateSelfSupportedProtocols.test.ts
+ jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(new Set(evaluatedProtocols));
+ jest.spyOn(userRepository, 'changeSupportedProtocols');
+
+ await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
+ expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols);
+ expect(userRepository.changeSupportedProtocols).not.toHaveBeenCalled();
+
+ const evaluatedProtocols2 = [ConversationProtocol.MLS];
+ jest
+ .spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols')
+ .mockResolvedValueOnce(new Set(evaluatedProtocols2));
+
+ await act(async () => {
+ jest.advanceTimersByTime(24 * 60 * 60 * 1000);
+ });
+
+ expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols2);
+ expect(userRepository.changeSupportedProtocols).toHaveBeenCalledWith(evaluatedProtocols2);
+ });
+});
diff --git a/src/script/mls/supportedProtocols/supportedProtocols.ts b/src/script/mls/supportedProtocols/supportedProtocols.ts
new file mode 100644
index 00000000000..0318f5f0661
--- /dev/null
+++ b/src/script/mls/supportedProtocols/supportedProtocols.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 {registerRecurringTask} from '@wireapp/core/lib/util/RecurringTaskScheduler';
+
+import {User} from 'src/script/entity/User';
+import {TeamRepository} from 'src/script/team/TeamRepository';
+import {UserRepository} from 'src/script/user/UserRepository';
+import {getLogger} from 'Util/Logger';
+import {TIME_IN_MILLIS} from 'Util/TimeUtil';
+
+import {evaluateSelfSupportedProtocols} from './evaluateSelfSupportedProtocols';
+
+const SELF_SUPPORTED_PROTOCOLS_CHECK_KEY = 'self-supported-protocols-check';
+
+const logger = getLogger('SupportedProtocols');
+
+/**
+ * Will initialise the intervals for checking (and updating if necessary) self supported protocols.
+ * Should be called only once on app load.
+ *
+ * @param selfUser - self user
+ * @param teamState - team state
+ * @param userRepository - user repository
+ */
+export const initialisePeriodicSelfSupportedProtocolsCheck = async (
+ selfUser: User,
+ {userRepository, teamRepository}: {userRepository: UserRepository; teamRepository: TeamRepository},
+) => {
+ const checkSupportedProtocolsTask = () => updateSelfSupportedProtocols(selfUser, {teamRepository, userRepository});
+
+ // We update supported protocols of self user on initial app load and then in 24 hours intervals
+ await checkSupportedProtocolsTask();
+
+ return registerRecurringTask({
+ every: TIME_IN_MILLIS.DAY,
+ task: checkSupportedProtocolsTask,
+ key: SELF_SUPPORTED_PROTOCOLS_CHECK_KEY,
+ });
+};
+
+const updateSelfSupportedProtocols = async (
+ selfUser: User,
+ {
+ userRepository,
+ teamRepository,
+ }: {
+ userRepository: UserRepository;
+ teamRepository: TeamRepository;
+ },
+) => {
+ const localSupportedProtocols = new Set(selfUser.supportedProtocols());
+ logger.info('Evaluating self supported protocols, currently supported protocols:', localSupportedProtocols);
+
+ try {
+ const refreshedSupportedProtocols = await evaluateSelfSupportedProtocols({teamRepository, userRepository});
+
+ const hasSupportedProtocolsChanged = !(
+ localSupportedProtocols.size === refreshedSupportedProtocols.size &&
+ [...localSupportedProtocols].every(protocol => refreshedSupportedProtocols.has(protocol))
+ );
+
+ if (!hasSupportedProtocolsChanged) {
+ return;
+ }
+
+ logger.info('Supported protocols will get updated to:', refreshedSupportedProtocols);
+ await userRepository.changeSupportedProtocols(Array.from(refreshedSupportedProtocols));
+ } catch (error) {
+ logger.error('Failed to update self supported protocols, will retry after 24h. Error: ', error);
+ }
+};
diff --git a/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/index.ts b/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/index.ts
new file mode 100644
index 00000000000..3c4c07542c2
--- /dev/null
+++ b/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/index.ts
@@ -0,0 +1,20 @@
+/*
+ * 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/.
+ *
+ */
+
+export * from './wasClientActiveWithinLast4Weeks';
diff --git a/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.test.ts b/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.test.ts
new file mode 100644
index 00000000000..fe8063daeba
--- /dev/null
+++ b/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.test.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 {RegisteredClient} from '@wireapp/api-client/lib/client';
+
+import {TIME_IN_MILLIS} from 'Util/TimeUtil';
+
+import {wasClientActiveWithinLast4Weeks} from './wasClientActiveWithinLast4Weeks';
+
+const createClientWithLastActive = (date: Date): RegisteredClient => {
+ return {last_active: date.toISOString()} as RegisteredClient;
+};
+
+describe('wasClientActiveWithinLast4Weeks', () => {
+ it('should return false if client was last active 29 days ago', () => {
+ const client = createClientWithLastActive(new Date(Date.now() - 29 * TIME_IN_MILLIS.DAY));
+ expect(wasClientActiveWithinLast4Weeks(client)).toBe(false);
+ });
+
+ it('should return true if client was last active exactly 28 days ago', () => {
+ const client = createClientWithLastActive(new Date(Date.now() - 28 * TIME_IN_MILLIS.DAY));
+ expect(wasClientActiveWithinLast4Weeks(client)).toBe(true);
+ });
+
+ it('should return true if client was last active less than 28 days ago', () => {
+ const client = createClientWithLastActive(new Date(Date.now() - 20 * TIME_IN_MILLIS.DAY));
+ expect(wasClientActiveWithinLast4Weeks(client)).toBe(true);
+ });
+});
diff --git a/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.ts b/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.ts
new file mode 100644
index 00000000000..35c45b28b65
--- /dev/null
+++ b/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 {RegisteredClient} from '@wireapp/api-client/lib/client';
+
+import {weeksPassedSinceDate} from 'Util/TimeUtil';
+
+export const wasClientActiveWithinLast4Weeks = ({last_active: lastActiveISODate}: RegisteredClient): boolean => {
+ //if client has not requested /notifications endpoint yet with backend supporting last_active field, we assume it is not active
+ if (!lastActiveISODate) {
+ return false;
+ }
+
+ const passedWeeksSinceLastActive = weeksPassedSinceDate(new Date(lastActiveISODate));
+ return passedWeeksSinceLastActive <= 4;
+};
From 6a10d0f12c50c6c15b7e72e239fc75854544365d Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 24 Jul 2023 13:08:38 +0200
Subject: [PATCH 02/70] runfix: initialise supported protocols with null
---
.../evaluateSelfSupportedProtocols.test.ts | 36 +++++++++----------
.../evaluateSelfSupportedProtocols.ts | 22 ++++++------
.../supportedProtocols.test.ts | 18 +++++-----
.../supportedProtocols/supportedProtocols.ts | 16 +++++----
4 files changed, 48 insertions(+), 44 deletions(-)
diff --git a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts
index 94419d97351..2247b33318d 100644
--- a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts
+++ b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts
@@ -117,64 +117,64 @@ const testScenarios = [
//we expect the following result based on whether all active clients are MLS capable or not
{
- allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS]),
- someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS],
+ someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
},
],
[
generateMLSFeaturesConfig(MLSMigrationStatus.DISABLED, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
{
- allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
+ someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
},
],
[
generateMLSFeaturesConfig(MLSMigrationStatus.DISABLED, [ConversationProtocol.MLS]),
{
- allActiveClientsMLSCapable: new Set([ConversationProtocol.MLS]),
- someActiveClientsNotMLSCapable: new Set([ConversationProtocol.MLS]),
+ allActiveClientsMLSCapable: [ConversationProtocol.MLS],
+ someActiveClientsNotMLSCapable: [ConversationProtocol.MLS],
},
],
[
generateMLSFeaturesConfig(MLSMigrationStatus.NOT_STARTED, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
{
- allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
+ someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
},
],
[
generateMLSFeaturesConfig(MLSMigrationStatus.NOT_STARTED, [ConversationProtocol.MLS]),
{
- allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
+ someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
},
],
[
generateMLSFeaturesConfig(MLSMigrationStatus.ONGOING, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
{
- allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
+ someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
},
],
[
generateMLSFeaturesConfig(MLSMigrationStatus.ONGOING, [ConversationProtocol.MLS]),
{
- allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS]),
+ allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
+ someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
},
],
[
generateMLSFeaturesConfig(MLSMigrationStatus.FINALISED, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
{
- allActiveClientsMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- someActiveClientsNotMLSCapable: new Set([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
+ allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
+ someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
},
],
[
generateMLSFeaturesConfig(MLSMigrationStatus.FINALISED, [ConversationProtocol.MLS]),
{
- allActiveClientsMLSCapable: new Set([ConversationProtocol.MLS]),
- someActiveClientsNotMLSCapable: new Set([ConversationProtocol.MLS]),
+ allActiveClientsMLSCapable: [ConversationProtocol.MLS],
+ someActiveClientsNotMLSCapable: [ConversationProtocol.MLS],
},
],
] as const;
diff --git a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts
index 028ea4c02b9..b7428f71c66 100644
--- a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts
+++ b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts
@@ -33,8 +33,8 @@ export const evaluateSelfSupportedProtocols = async ({
}: {
teamRepository: TeamRepository;
userRepository: UserRepository;
-}): Promise> => {
- const supportedProtocols = new Set();
+}): Promise => {
+ const supportedProtocols: ConversationProtocol[] = [];
const teamSupportedProtocols = teamRepository.getTeamSupportedProtocols();
const mlsMigrationStatus = teamRepository.getTeamMLSMigrationStatus();
@@ -43,7 +43,7 @@ export const evaluateSelfSupportedProtocols = async ({
const isProteusProtocolSupported = await isProteusSupported({teamSupportedProtocols, mlsMigrationStatus});
if (isProteusProtocolSupported) {
- supportedProtocols.add(ConversationProtocol.PROTEUS);
+ supportedProtocols.push(ConversationProtocol.PROTEUS);
}
const mlsCheckDependencies = {
@@ -57,7 +57,7 @@ export const evaluateSelfSupportedProtocols = async ({
const isMLSForced = await isMLSForcedWithoutMigration(mlsCheckDependencies);
if (isMLSProtocolSupported || isMLSForced) {
- supportedProtocols.add(ConversationProtocol.MLS);
+ supportedProtocols.push(ConversationProtocol.MLS);
}
return supportedProtocols;
@@ -73,7 +73,7 @@ const isMLSSupported = async ({
selfClients,
mlsMigrationStatus,
}: {
- teamSupportedProtocols: Set;
+ teamSupportedProtocols: ConversationProtocol[];
selfClients: RegisteredClient[];
mlsMigrationStatus: MLSMigrationStatus;
}): Promise => {
@@ -83,7 +83,7 @@ const isMLSSupported = async ({
return false;
}
- const isMLSSupportedByTeam = teamSupportedProtocols.has(ConversationProtocol.MLS);
+ const isMLSSupportedByTeam = teamSupportedProtocols.includes(ConversationProtocol.MLS);
const doActiveClientsSupportMLS = await haveAllActiveClientsRegisteredMLSDevice(selfClients);
return isMLSSupportedByTeam && (doActiveClientsSupportMLS || mlsMigrationStatus === MLSMigrationStatus.FINALISED);
};
@@ -100,7 +100,7 @@ const isMLSForcedWithoutMigration = async ({
selfClients,
mlsMigrationStatus,
}: {
- teamSupportedProtocols: Set;
+ teamSupportedProtocols: ConversationProtocol[];
selfClients: RegisteredClient[];
mlsMigrationStatus: MLSMigrationStatus;
}): Promise => {
@@ -110,8 +110,8 @@ const isMLSForcedWithoutMigration = async ({
return false;
}
- const isMLSSupportedByTeam = teamSupportedProtocols.has(ConversationProtocol.MLS);
- const isProteusSupportedByTeam = teamSupportedProtocols.has(ConversationProtocol.PROTEUS);
+ const isMLSSupportedByTeam = teamSupportedProtocols.includes(ConversationProtocol.MLS);
+ const isProteusSupportedByTeam = teamSupportedProtocols.includes(ConversationProtocol.PROTEUS);
const doActiveClientsSupportMLS = await haveAllActiveClientsRegisteredMLSDevice(selfClients);
const isMigrationDisabled = mlsMigrationStatus === MLSMigrationStatus.DISABLED;
@@ -127,10 +127,10 @@ const isProteusSupported = async ({
teamSupportedProtocols,
mlsMigrationStatus,
}: {
- teamSupportedProtocols: Set;
+ teamSupportedProtocols: ConversationProtocol[];
mlsMigrationStatus: MLSMigrationStatus;
}): Promise => {
- const isProteusSupportedByTeam = teamSupportedProtocols.has(ConversationProtocol.PROTEUS);
+ const isProteusSupportedByTeam = teamSupportedProtocols.includes(ConversationProtocol.PROTEUS);
return (
isProteusSupportedByTeam ||
[MLSMigrationStatus.NOT_STARTED, MLSMigrationStatus.ONGOING].includes(mlsMigrationStatus)
diff --git a/src/script/mls/supportedProtocols/supportedProtocols.test.ts b/src/script/mls/supportedProtocols/supportedProtocols.test.ts
index 8757bd1af3d..ec67b282f07 100644
--- a/src/script/mls/supportedProtocols/supportedProtocols.test.ts
+++ b/src/script/mls/supportedProtocols/supportedProtocols.test.ts
@@ -38,8 +38,8 @@ describe('supportedProtocols', () => {
it.each([
[[ConversationProtocol.PROTEUS], [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]],
- [[ConversationProtocol.PROTEUS, ConversationProtocol.MLS], [ConversationProtocol.PROTEUS]],
- [[ConversationProtocol.PROTEUS], [ConversationProtocol.MLS]],
+ // [[ConversationProtocol.PROTEUS, ConversationProtocol.MLS], [ConversationProtocol.PROTEUS]],
+ // [[ConversationProtocol.PROTEUS], [ConversationProtocol.MLS]],
])('Updates the list of supported protocols', async (initialProtocols, evaluatedProtocols) => {
const userRepository = await testFactory.exposeUserActors();
const teamRepository = await testFactory.exposeTeamActors();
@@ -49,10 +49,12 @@ describe('supportedProtocols', () => {
selfUser.supportedProtocols(initialProtocols);
//this funciton is tested standalone in evaluateSelfSupportedProtocols.test.ts
- jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(new Set(evaluatedProtocols));
+ jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols);
jest.spyOn(userRepository, 'changeSupportedProtocols');
- await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
+ await act(async () => {
+ await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
+ });
expect(userRepository.changeSupportedProtocols).toHaveBeenCalledWith(evaluatedProtocols);
expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols);
@@ -70,7 +72,7 @@ describe('supportedProtocols', () => {
const evaluatedProtocols = [ConversationProtocol.PROTEUS];
//this funciton is tested standalone in evaluateSelfSupportedProtocols.test.ts
- jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(new Set(evaluatedProtocols));
+ jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols);
jest.spyOn(userRepository, 'changeSupportedProtocols');
await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
@@ -89,7 +91,7 @@ describe('supportedProtocols', () => {
const evaluatedProtocols = [ConversationProtocol.PROTEUS];
//this funciton is tested standalone in evaluateSelfSupportedProtocols.test.ts
- jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(new Set(evaluatedProtocols));
+ jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols);
jest.spyOn(userRepository, 'changeSupportedProtocols');
await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
@@ -97,9 +99,7 @@ describe('supportedProtocols', () => {
expect(userRepository.changeSupportedProtocols).not.toHaveBeenCalled();
const evaluatedProtocols2 = [ConversationProtocol.MLS];
- jest
- .spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols')
- .mockResolvedValueOnce(new Set(evaluatedProtocols2));
+ jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols2);
await act(async () => {
jest.advanceTimersByTime(24 * 60 * 60 * 1000);
diff --git a/src/script/mls/supportedProtocols/supportedProtocols.ts b/src/script/mls/supportedProtocols/supportedProtocols.ts
index 0318f5f0661..8513876f1de 100644
--- a/src/script/mls/supportedProtocols/supportedProtocols.ts
+++ b/src/script/mls/supportedProtocols/supportedProtocols.ts
@@ -64,24 +64,28 @@ const updateSelfSupportedProtocols = async (
userRepository: UserRepository;
teamRepository: TeamRepository;
},
-) => {
- const localSupportedProtocols = new Set(selfUser.supportedProtocols());
+): Promise => {
+ const localSupportedProtocols = selfUser.supportedProtocols();
+
logger.info('Evaluating self supported protocols, currently supported protocols:', localSupportedProtocols);
try {
const refreshedSupportedProtocols = await evaluateSelfSupportedProtocols({teamRepository, userRepository});
+ if (!localSupportedProtocols) {
+ return void userRepository.changeSupportedProtocols(refreshedSupportedProtocols);
+ }
+
const hasSupportedProtocolsChanged = !(
- localSupportedProtocols.size === refreshedSupportedProtocols.size &&
- [...localSupportedProtocols].every(protocol => refreshedSupportedProtocols.has(protocol))
+ localSupportedProtocols.length === refreshedSupportedProtocols.length &&
+ [...localSupportedProtocols].every(protocol => refreshedSupportedProtocols.includes(protocol))
);
if (!hasSupportedProtocolsChanged) {
return;
}
- logger.info('Supported protocols will get updated to:', refreshedSupportedProtocols);
- await userRepository.changeSupportedProtocols(Array.from(refreshedSupportedProtocols));
+ return void userRepository.changeSupportedProtocols(refreshedSupportedProtocols);
} catch (error) {
logger.error('Failed to update self supported protocols, will retry after 24h. Error: ', error);
}
From fa4ac9f7e5aac298cb06298c3271c8ae18fced11 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 24 Jul 2023 14:46:52 +0200
Subject: [PATCH 03/70] test: uncomment scenarios
---
src/script/mls/supportedProtocols/supportedProtocols.test.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/script/mls/supportedProtocols/supportedProtocols.test.ts b/src/script/mls/supportedProtocols/supportedProtocols.test.ts
index ec67b282f07..146d97fccf5 100644
--- a/src/script/mls/supportedProtocols/supportedProtocols.test.ts
+++ b/src/script/mls/supportedProtocols/supportedProtocols.test.ts
@@ -38,8 +38,8 @@ describe('supportedProtocols', () => {
it.each([
[[ConversationProtocol.PROTEUS], [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]],
- // [[ConversationProtocol.PROTEUS, ConversationProtocol.MLS], [ConversationProtocol.PROTEUS]],
- // [[ConversationProtocol.PROTEUS], [ConversationProtocol.MLS]],
+ [[ConversationProtocol.PROTEUS, ConversationProtocol.MLS], [ConversationProtocol.PROTEUS]],
+ [[ConversationProtocol.PROTEUS], [ConversationProtocol.MLS]],
])('Updates the list of supported protocols', async (initialProtocols, evaluatedProtocols) => {
const userRepository = await testFactory.exposeUserActors();
const teamRepository = await testFactory.exposeTeamActors();
From 04ee8d99313efd0b6ae2c7902918c634e3545fc9 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Wed, 26 Jul 2023 11:29:00 +0900
Subject: [PATCH 04/70] refactor: move self supported protocols to self
repository
---
src/script/main/app.ts | 1 -
.../evaluateSelfSupportedProtocols.test.ts | 212 ------------------
.../evaluateSelfSupportedProtocols.ts | 144 ------------
.../evaluateSelfSupportedProtocols/index.ts | 20 --
src/script/mls/supportedProtocols/index.ts | 20 --
.../supportedProtocols.test.ts | 111 ---------
.../supportedProtocols/supportedProtocols.ts | 92 --------
.../wasClientActiveWithinLast4Weeks/index.ts | 20 --
.../wasClientActiveWithinLast4Weeks.test.ts | 45 ----
.../wasClientActiveWithinLast4Weeks.ts | 32 ---
10 files changed, 697 deletions(-)
delete mode 100644 src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts
delete mode 100644 src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts
delete mode 100644 src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/index.ts
delete mode 100644 src/script/mls/supportedProtocols/index.ts
delete mode 100644 src/script/mls/supportedProtocols/supportedProtocols.test.ts
delete mode 100644 src/script/mls/supportedProtocols/supportedProtocols.ts
delete mode 100644 src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/index.ts
delete mode 100644 src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.test.ts
delete mode 100644 src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.ts
diff --git a/src/script/main/app.ts b/src/script/main/app.ts
index 7166172c2cf..398dff87351 100644
--- a/src/script/main/app.ts
+++ b/src/script/main/app.ts
@@ -81,7 +81,6 @@ import {IntegrationService} from '../integration/IntegrationService';
import {startNewVersionPolling} from '../lifecycle/newVersionHandler';
import {MediaRepository} from '../media/MediaRepository';
import {initMLSCallbacks, initMLSConversations, registerUninitializedSelfAndTeamConversations} from '../mls';
-import {initialisePeriodicSelfSupportedProtocolsCheck} from '../mls/supportedProtocols';
import {NotificationRepository} from '../notification/NotificationRepository';
import {PreferenceNotificationRepository} from '../notification/PreferenceNotificationRepository';
import {PermissionRepository} from '../permission/PermissionRepository';
diff --git a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts
deleted file mode 100644
index 2247b33318d..00000000000
--- a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.test.ts
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * 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 {RegisteredClient} from '@wireapp/api-client/lib/client';
-import {ConversationProtocol} from '@wireapp/api-client/lib/conversation';
-import {FeatureList, FeatureStatus} from '@wireapp/api-client/lib/team';
-
-import {TestFactory} from 'test/helper/TestFactory';
-import {TIME_IN_MILLIS} from 'Util/TimeUtil';
-
-import {evaluateSelfSupportedProtocols} from './evaluateSelfSupportedProtocols';
-
-import * as mlsSupport from '../../isMLSSupportedByEnvironment';
-import {MLSMigrationStatus} from '../../MLSMigration/migrationStatus';
-
-const testFactory = new TestFactory();
-
-jest.spyOn(mlsSupport, 'isMLSSupportedByEnvironment').mockResolvedValue(true);
-
-const generateMLSFeaturesConfig = (migrationStatus: MLSMigrationStatus, supportedProtocols: ConversationProtocol[]) => {
- const now = Date.now();
-
- switch (migrationStatus) {
- case MLSMigrationStatus.DISABLED:
- return {
- mls: {
- status: FeatureStatus.ENABLED,
- config: {supportedProtocols},
- },
- mlsMigration: {status: FeatureStatus.DISABLED, config: {}},
- };
- case MLSMigrationStatus.NOT_STARTED:
- return {
- mls: {
- status: FeatureStatus.ENABLED,
- config: {supportedProtocols},
- },
- mlsMigration: {
- status: FeatureStatus.ENABLED,
- config: {
- startTime: new Date(now + 1 * TIME_IN_MILLIS.DAY).toISOString(),
- finaliseRegardlessAfter: new Date(now + 2 * TIME_IN_MILLIS.DAY).toISOString(),
- },
- },
- };
- case MLSMigrationStatus.ONGOING:
- return {
- mls: {
- status: FeatureStatus.ENABLED,
- config: {supportedProtocols},
- },
- mlsMigration: {
- status: FeatureStatus.ENABLED,
- config: {
- startTime: new Date(now - 1 * TIME_IN_MILLIS.DAY).toISOString(),
- finaliseRegardlessAfter: new Date(now + 1 * TIME_IN_MILLIS.DAY).toISOString(),
- },
- },
- };
- case MLSMigrationStatus.FINALISED:
- return {
- mls: {
- status: FeatureStatus.ENABLED,
- config: {supportedProtocols},
- },
- mlsMigration: {
- status: FeatureStatus.ENABLED,
- config: {
- startTime: new Date(now - 2 * TIME_IN_MILLIS.DAY).toISOString(),
- finaliseRegardlessAfter: new Date(now - 1 * TIME_IN_MILLIS.DAY).toISOString(),
- },
- },
- };
- }
-};
-
-const createMockClientResponse = (doesSupportMLS = false, wasActiveWithinLast4Weeks = false) => {
- return {
- mls_public_keys: doesSupportMLS ? {ed25519: 'key'} : undefined,
- last_active: wasActiveWithinLast4Weeks
- ? new Date().toISOString()
- : new Date(Date.now() - 5 * 7 * 24 * 60 * 60 * 1000).toISOString(),
- } as unknown as RegisteredClient;
-};
-
-const generateListOfSelfClients = ({allActiveClientsMLSCapable}: {allActiveClientsMLSCapable: boolean}) => {
- const clients: RegisteredClient[] = [];
-
- new Array(4).fill(0).forEach(() => clients.push(createMockClientResponse(true, true)));
- if (!allActiveClientsMLSCapable) {
- new Array(2).fill(0).forEach(() => clients.push(createMockClientResponse(false, true)));
- }
-
- return clients;
-};
-
-const testScenarios = [
- [
- //with given config
- generateMLSFeaturesConfig(MLSMigrationStatus.DISABLED, [ConversationProtocol.PROTEUS]),
-
- //we expect the following result based on whether all active clients are MLS capable or not
- {
- allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS],
- someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
- },
- ],
- [
- generateMLSFeaturesConfig(MLSMigrationStatus.DISABLED, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- {
- allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
- someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
- },
- ],
- [
- generateMLSFeaturesConfig(MLSMigrationStatus.DISABLED, [ConversationProtocol.MLS]),
- {
- allActiveClientsMLSCapable: [ConversationProtocol.MLS],
- someActiveClientsNotMLSCapable: [ConversationProtocol.MLS],
- },
- ],
- [
- generateMLSFeaturesConfig(MLSMigrationStatus.NOT_STARTED, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- {
- allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
- someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
- },
- ],
- [
- generateMLSFeaturesConfig(MLSMigrationStatus.NOT_STARTED, [ConversationProtocol.MLS]),
- {
- allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
- someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
- },
- ],
- [
- generateMLSFeaturesConfig(MLSMigrationStatus.ONGOING, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- {
- allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
- someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
- },
- ],
- [
- generateMLSFeaturesConfig(MLSMigrationStatus.ONGOING, [ConversationProtocol.MLS]),
- {
- allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
- someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS],
- },
- ],
- [
- generateMLSFeaturesConfig(MLSMigrationStatus.FINALISED, [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]),
- {
- allActiveClientsMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
- someActiveClientsNotMLSCapable: [ConversationProtocol.PROTEUS, ConversationProtocol.MLS],
- },
- ],
- [
- generateMLSFeaturesConfig(MLSMigrationStatus.FINALISED, [ConversationProtocol.MLS]),
- {
- allActiveClientsMLSCapable: [ConversationProtocol.MLS],
- someActiveClientsNotMLSCapable: [ConversationProtocol.MLS],
- },
- ],
-] as const;
-
-describe('evaluateSelfSupportedProtocols', () => {
- describe.each([{allActiveClientsMLSCapable: true}, {allActiveClientsMLSCapable: false}])(
- '%o',
- ({allActiveClientsMLSCapable}) => {
- const selfClients = generateListOfSelfClients({allActiveClientsMLSCapable});
-
- it.each(testScenarios)('evaluates self supported protocols', async ({mls, mlsMigration}, expected) => {
- const teamRepository = await testFactory.exposeTeamActors();
- const userRepository = await testFactory.exposeUserActors();
-
- jest.spyOn(userRepository, 'getAllSelfClients').mockResolvedValueOnce(selfClients);
-
- const teamFeatureList = {
- mlsMigration,
- mls,
- } as unknown as FeatureList;
-
- jest.spyOn(teamRepository['teamState'], 'teamFeatures').mockReturnValue(teamFeatureList);
-
- const supportedProtocols = await evaluateSelfSupportedProtocols({
- teamRepository,
- userRepository,
- });
-
- expect(supportedProtocols).toEqual(
- allActiveClientsMLSCapable ? expected.allActiveClientsMLSCapable : expected.someActiveClientsNotMLSCapable,
- );
- });
- },
- );
-});
diff --git a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts
deleted file mode 100644
index b7428f71c66..00000000000
--- a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * 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 {RegisteredClient} from '@wireapp/api-client/lib/client';
-import {ConversationProtocol} from '@wireapp/api-client/lib/conversation';
-
-import {TeamRepository} from 'src/script/team/TeamRepository';
-import {UserRepository} from 'src/script/user/UserRepository';
-
-import {isMLSSupportedByEnvironment} from '../../isMLSSupportedByEnvironment';
-import {MLSMigrationStatus} from '../../MLSMigration/migrationStatus';
-import {wasClientActiveWithinLast4Weeks} from '../wasClientActiveWithinLast4Weeks';
-
-export const evaluateSelfSupportedProtocols = async ({
- teamRepository,
- userRepository,
-}: {
- teamRepository: TeamRepository;
- userRepository: UserRepository;
-}): Promise => {
- const supportedProtocols: ConversationProtocol[] = [];
-
- const teamSupportedProtocols = teamRepository.getTeamSupportedProtocols();
- const mlsMigrationStatus = teamRepository.getTeamMLSMigrationStatus();
-
- const selfClients = await userRepository.getAllSelfClients();
-
- const isProteusProtocolSupported = await isProteusSupported({teamSupportedProtocols, mlsMigrationStatus});
- if (isProteusProtocolSupported) {
- supportedProtocols.push(ConversationProtocol.PROTEUS);
- }
-
- const mlsCheckDependencies = {
- teamSupportedProtocols,
- selfClients,
- mlsMigrationStatus,
- };
-
- const isMLSProtocolSupported = await isMLSSupported(mlsCheckDependencies);
-
- const isMLSForced = await isMLSForcedWithoutMigration(mlsCheckDependencies);
-
- if (isMLSProtocolSupported || isMLSForced) {
- supportedProtocols.push(ConversationProtocol.MLS);
- }
-
- return supportedProtocols;
-};
-
-/*
- MLS is supported if:
- - MLS is in the list of supported protocols
- - All active clients support MLS, or MLS migration is finalised
-*/
-const isMLSSupported = async ({
- teamSupportedProtocols,
- selfClients,
- mlsMigrationStatus,
-}: {
- teamSupportedProtocols: ConversationProtocol[];
- selfClients: RegisteredClient[];
- mlsMigrationStatus: MLSMigrationStatus;
-}): Promise => {
- const isMLSSupportedByEnv = await isMLSSupportedByEnvironment();
-
- if (!isMLSSupportedByEnv) {
- return false;
- }
-
- const isMLSSupportedByTeam = teamSupportedProtocols.includes(ConversationProtocol.MLS);
- const doActiveClientsSupportMLS = await haveAllActiveClientsRegisteredMLSDevice(selfClients);
- return isMLSSupportedByTeam && (doActiveClientsSupportMLS || mlsMigrationStatus === MLSMigrationStatus.FINALISED);
-};
-
-/*
- MLS is forced if:
- - only MLS is in the list of supported protocols
- - MLS migration is disabled
- - There are still some active clients that do not support MLS
- It means that team admin wants to force MLS and drop proteus support, even though not all active clients support MLS
-*/
-const isMLSForcedWithoutMigration = async ({
- teamSupportedProtocols,
- selfClients,
- mlsMigrationStatus,
-}: {
- teamSupportedProtocols: ConversationProtocol[];
- selfClients: RegisteredClient[];
- mlsMigrationStatus: MLSMigrationStatus;
-}): Promise => {
- const isMLSSupportedByEnv = await isMLSSupportedByEnvironment();
-
- if (!isMLSSupportedByEnv) {
- return false;
- }
-
- const isMLSSupportedByTeam = teamSupportedProtocols.includes(ConversationProtocol.MLS);
- const isProteusSupportedByTeam = teamSupportedProtocols.includes(ConversationProtocol.PROTEUS);
- const doActiveClientsSupportMLS = await haveAllActiveClientsRegisteredMLSDevice(selfClients);
- const isMigrationDisabled = mlsMigrationStatus === MLSMigrationStatus.DISABLED;
-
- return !doActiveClientsSupportMLS && isMLSSupportedByTeam && !isProteusSupportedByTeam && isMigrationDisabled;
-};
-
-/*
- Proteus is supported if:
- - Proteus is in the list of supported protocols
- - MLS migration is enabled but not finalised
-*/
-const isProteusSupported = async ({
- teamSupportedProtocols,
- mlsMigrationStatus,
-}: {
- teamSupportedProtocols: ConversationProtocol[];
- mlsMigrationStatus: MLSMigrationStatus;
-}): Promise => {
- const isProteusSupportedByTeam = teamSupportedProtocols.includes(ConversationProtocol.PROTEUS);
- return (
- isProteusSupportedByTeam ||
- [MLSMigrationStatus.NOT_STARTED, MLSMigrationStatus.ONGOING].includes(mlsMigrationStatus)
- );
-};
-
-const haveAllActiveClientsRegisteredMLSDevice = async (selfClients: RegisteredClient[]): Promise => {
- //we consider client active if it was active within last 4 weeks
- const activeClients = selfClients.filter(wasClientActiveWithinLast4Weeks);
- return activeClients.every(client => !!client.mls_public_keys);
-};
diff --git a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/index.ts b/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/index.ts
deleted file mode 100644
index d106ff936e5..00000000000
--- a/src/script/mls/supportedProtocols/evaluateSelfSupportedProtocols/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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/.
- *
- */
-
-export * from './evaluateSelfSupportedProtocols';
diff --git a/src/script/mls/supportedProtocols/index.ts b/src/script/mls/supportedProtocols/index.ts
deleted file mode 100644
index f6b2b21e58c..00000000000
--- a/src/script/mls/supportedProtocols/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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/.
- *
- */
-
-export * from './supportedProtocols';
diff --git a/src/script/mls/supportedProtocols/supportedProtocols.test.ts b/src/script/mls/supportedProtocols/supportedProtocols.test.ts
deleted file mode 100644
index 146d97fccf5..00000000000
--- a/src/script/mls/supportedProtocols/supportedProtocols.test.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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 {ConversationProtocol} from '@wireapp/api-client/lib/conversation';
-import {act} from 'react-dom/test-utils';
-
-import {TestFactory} from 'test/helper/TestFactory';
-
-import * as supportedProtocols from './evaluateSelfSupportedProtocols/evaluateSelfSupportedProtocols';
-import {initialisePeriodicSelfSupportedProtocolsCheck} from './supportedProtocols';
-
-const testFactory = new TestFactory();
-
-describe('supportedProtocols', () => {
- beforeAll(() => {
- jest.useFakeTimers();
- });
-
- afterAll(() => {
- jest.useRealTimers();
- });
-
- it.each([
- [[ConversationProtocol.PROTEUS], [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]],
- [[ConversationProtocol.PROTEUS, ConversationProtocol.MLS], [ConversationProtocol.PROTEUS]],
- [[ConversationProtocol.PROTEUS], [ConversationProtocol.MLS]],
- ])('Updates the list of supported protocols', async (initialProtocols, evaluatedProtocols) => {
- const userRepository = await testFactory.exposeUserActors();
- const teamRepository = await testFactory.exposeTeamActors();
-
- const selfUser = userRepository['userState'].self();
-
- selfUser.supportedProtocols(initialProtocols);
-
- //this funciton is tested standalone in evaluateSelfSupportedProtocols.test.ts
- jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols);
- jest.spyOn(userRepository, 'changeSupportedProtocols');
-
- await act(async () => {
- await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
- });
-
- expect(userRepository.changeSupportedProtocols).toHaveBeenCalledWith(evaluatedProtocols);
- expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols);
- });
-
- it("Does not update supported protocols if they didn't change", async () => {
- const userRepository = await testFactory.exposeUserActors();
- const teamRepository = await testFactory.exposeTeamActors();
-
- const selfUser = userRepository['userState'].self();
-
- const initialProtocols = [ConversationProtocol.PROTEUS];
- selfUser.supportedProtocols(initialProtocols);
-
- const evaluatedProtocols = [ConversationProtocol.PROTEUS];
-
- //this funciton is tested standalone in evaluateSelfSupportedProtocols.test.ts
- jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols);
- jest.spyOn(userRepository, 'changeSupportedProtocols');
-
- await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
- expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols);
- expect(userRepository.changeSupportedProtocols).not.toHaveBeenCalled();
- });
-
- it('Re-evaluates supported protocols every 24h', async () => {
- const userRepository = await testFactory.exposeUserActors();
- const teamRepository = await testFactory.exposeTeamActors();
- const selfUser = userRepository['userState'].self();
-
- const initialProtocols = [ConversationProtocol.PROTEUS];
- selfUser.supportedProtocols(initialProtocols);
-
- const evaluatedProtocols = [ConversationProtocol.PROTEUS];
-
- //this funciton is tested standalone in evaluateSelfSupportedProtocols.test.ts
- jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols);
- jest.spyOn(userRepository, 'changeSupportedProtocols');
-
- await initialisePeriodicSelfSupportedProtocolsCheck(selfUser, {userRepository, teamRepository});
- expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols);
- expect(userRepository.changeSupportedProtocols).not.toHaveBeenCalled();
-
- const evaluatedProtocols2 = [ConversationProtocol.MLS];
- jest.spyOn(supportedProtocols, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols2);
-
- await act(async () => {
- jest.advanceTimersByTime(24 * 60 * 60 * 1000);
- });
-
- expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols2);
- expect(userRepository.changeSupportedProtocols).toHaveBeenCalledWith(evaluatedProtocols2);
- });
-});
diff --git a/src/script/mls/supportedProtocols/supportedProtocols.ts b/src/script/mls/supportedProtocols/supportedProtocols.ts
deleted file mode 100644
index 8513876f1de..00000000000
--- a/src/script/mls/supportedProtocols/supportedProtocols.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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 {registerRecurringTask} from '@wireapp/core/lib/util/RecurringTaskScheduler';
-
-import {User} from 'src/script/entity/User';
-import {TeamRepository} from 'src/script/team/TeamRepository';
-import {UserRepository} from 'src/script/user/UserRepository';
-import {getLogger} from 'Util/Logger';
-import {TIME_IN_MILLIS} from 'Util/TimeUtil';
-
-import {evaluateSelfSupportedProtocols} from './evaluateSelfSupportedProtocols';
-
-const SELF_SUPPORTED_PROTOCOLS_CHECK_KEY = 'self-supported-protocols-check';
-
-const logger = getLogger('SupportedProtocols');
-
-/**
- * Will initialise the intervals for checking (and updating if necessary) self supported protocols.
- * Should be called only once on app load.
- *
- * @param selfUser - self user
- * @param teamState - team state
- * @param userRepository - user repository
- */
-export const initialisePeriodicSelfSupportedProtocolsCheck = async (
- selfUser: User,
- {userRepository, teamRepository}: {userRepository: UserRepository; teamRepository: TeamRepository},
-) => {
- const checkSupportedProtocolsTask = () => updateSelfSupportedProtocols(selfUser, {teamRepository, userRepository});
-
- // We update supported protocols of self user on initial app load and then in 24 hours intervals
- await checkSupportedProtocolsTask();
-
- return registerRecurringTask({
- every: TIME_IN_MILLIS.DAY,
- task: checkSupportedProtocolsTask,
- key: SELF_SUPPORTED_PROTOCOLS_CHECK_KEY,
- });
-};
-
-const updateSelfSupportedProtocols = async (
- selfUser: User,
- {
- userRepository,
- teamRepository,
- }: {
- userRepository: UserRepository;
- teamRepository: TeamRepository;
- },
-): Promise => {
- const localSupportedProtocols = selfUser.supportedProtocols();
-
- logger.info('Evaluating self supported protocols, currently supported protocols:', localSupportedProtocols);
-
- try {
- const refreshedSupportedProtocols = await evaluateSelfSupportedProtocols({teamRepository, userRepository});
-
- if (!localSupportedProtocols) {
- return void userRepository.changeSupportedProtocols(refreshedSupportedProtocols);
- }
-
- const hasSupportedProtocolsChanged = !(
- localSupportedProtocols.length === refreshedSupportedProtocols.length &&
- [...localSupportedProtocols].every(protocol => refreshedSupportedProtocols.includes(protocol))
- );
-
- if (!hasSupportedProtocolsChanged) {
- return;
- }
-
- return void userRepository.changeSupportedProtocols(refreshedSupportedProtocols);
- } catch (error) {
- logger.error('Failed to update self supported protocols, will retry after 24h. Error: ', error);
- }
-};
diff --git a/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/index.ts b/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/index.ts
deleted file mode 100644
index 3c4c07542c2..00000000000
--- a/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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/.
- *
- */
-
-export * from './wasClientActiveWithinLast4Weeks';
diff --git a/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.test.ts b/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.test.ts
deleted file mode 100644
index fe8063daeba..00000000000
--- a/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.test.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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 {RegisteredClient} from '@wireapp/api-client/lib/client';
-
-import {TIME_IN_MILLIS} from 'Util/TimeUtil';
-
-import {wasClientActiveWithinLast4Weeks} from './wasClientActiveWithinLast4Weeks';
-
-const createClientWithLastActive = (date: Date): RegisteredClient => {
- return {last_active: date.toISOString()} as RegisteredClient;
-};
-
-describe('wasClientActiveWithinLast4Weeks', () => {
- it('should return false if client was last active 29 days ago', () => {
- const client = createClientWithLastActive(new Date(Date.now() - 29 * TIME_IN_MILLIS.DAY));
- expect(wasClientActiveWithinLast4Weeks(client)).toBe(false);
- });
-
- it('should return true if client was last active exactly 28 days ago', () => {
- const client = createClientWithLastActive(new Date(Date.now() - 28 * TIME_IN_MILLIS.DAY));
- expect(wasClientActiveWithinLast4Weeks(client)).toBe(true);
- });
-
- it('should return true if client was last active less than 28 days ago', () => {
- const client = createClientWithLastActive(new Date(Date.now() - 20 * TIME_IN_MILLIS.DAY));
- expect(wasClientActiveWithinLast4Weeks(client)).toBe(true);
- });
-});
diff --git a/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.ts b/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.ts
deleted file mode 100644
index 35c45b28b65..00000000000
--- a/src/script/mls/supportedProtocols/wasClientActiveWithinLast4Weeks/wasClientActiveWithinLast4Weeks.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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 {RegisteredClient} from '@wireapp/api-client/lib/client';
-
-import {weeksPassedSinceDate} from 'Util/TimeUtil';
-
-export const wasClientActiveWithinLast4Weeks = ({last_active: lastActiveISODate}: RegisteredClient): boolean => {
- //if client has not requested /notifications endpoint yet with backend supporting last_active field, we assume it is not active
- if (!lastActiveISODate) {
- return false;
- }
-
- const passedWeeksSinceLastActive = weeksPassedSinceDate(new Date(lastActiveISODate));
- return passedWeeksSinceLastActive <= 4;
-};
From 1758521a78a5b016309058db4f52c07690f84447 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 24 Jul 2023 09:16:08 +0200
Subject: [PATCH 05/70] feat: initial work on 1:1 mls conversations
---
.../conversation/ConversationRepository.ts | 139 +++++++++++++++++-
.../conversation/ConversationService.ts | 4 +
src/script/main/app.ts | 13 +-
src/script/self/SelfRepository.ts | 14 ++
src/script/user/UserRepository.ts | 24 ++-
src/script/user/UserService.ts | 8 +
6 files changed, 188 insertions(+), 14 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 4356e930c69..bd7c167b238 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -84,7 +84,7 @@ import {ConversationFilter} from './ConversationFilter';
import {ConversationLabelRepository} from './ConversationLabelRepository';
import {ConversationDatabaseData, ConversationMapper} from './ConversationMapper';
import {ConversationRoleRepository} from './ConversationRoleRepository';
-import {isMLSConversation} from './ConversationSelectors';
+import {isMLSConversation, MLSConversation} from './ConversationSelectors';
import {ConversationService} from './ConversationService';
import {ConversationState} from './ConversationState';
import {ConversationStateHandler} from './ConversationStateHandler';
@@ -136,6 +136,7 @@ import {SuperType} from '../message/SuperType';
import {SystemMessageType} from '../message/SystemMessageType';
import {addOtherSelfClientsToMLSConversation} from '../mls';
import {PropertiesRepository} from '../properties/PropertiesRepository';
+import {SelfRepository} from '../self/SelfRepository';
import {Core} from '../service/CoreSingleton';
import type {EventRecord} from '../storage';
import {ConversationRecord} from '../storage/record/ConversationRecord';
@@ -182,6 +183,7 @@ export class ConversationRepository {
private readonly eventRepository: EventRepository,
private readonly teamRepository: TeamRepository,
private readonly userRepository: UserRepository,
+ private readonly selfRepository: SelfRepository,
private readonly propertyRepository: PropertiesRepository,
private readonly callingRepository: CallingRepository,
private readonly serverTimeHandler: ServerTimeHandler,
@@ -641,7 +643,7 @@ export class ConversationRepository {
const response = await this.conversationService.getConversationById(qualifiedId);
const [conversationEntity] = this.mapConversations([response]);
- this.saveConversation(conversationEntity);
+ await this.saveConversation(conversationEntity);
fetching_conversations[conversationId].forEach(({resolveFn}) => resolveFn(conversationEntity));
delete fetching_conversations[conversationId];
@@ -709,6 +711,9 @@ export class ConversationRepository {
conversationsData = (await this.conversationService.saveConversationsInDb(data)) as any[];
}
+ //TODO: filter out proteus 1:1 conversations that should use 1:1 mls converstions instead
+ //when filtering them out change the conversation field of each message to the mls conversation id
+
const allConversationEntities = this.mapConversations(conversationsData);
const newConversationEntities = allConversationEntities.filter(
allConversations =>
@@ -1195,6 +1200,7 @@ export class ConversationRepository {
const conversationId = userEntity.connection().conversationId;
try {
+ //TODO: check if mls conversation will be returned when fetched with this endpoint
const conversationEntity = await this.getConversationById(conversationId);
conversationEntity.connection(userEntity.connection());
await this.updateParticipatingUserEntities(conversationEntity);
@@ -1351,11 +1357,135 @@ export class ConversationRepository {
}
};
- private readonly getConnectionConversation = (connectionEntity: ConnectionEntity) => {
- const {conversationId} = connectionEntity;
+ private readonly getProtocolFor1to1Conversation = async (otherUserId: QualifiedId) => {
+ const otherUserSupportedProtocolsSet = await this.userRepository.getUserSupportedProtocols(otherUserId);
+ const selfUserSupportedProtocolsSet = await this.selfRepository.getSelfSupportedProtocols();
+
+ const commonProtocols = otherUserSupportedProtocolsSet.filter(protocol =>
+ selfUserSupportedProtocolsSet.includes(protocol),
+ );
+
+ if (commonProtocols.includes(ConversationProtocol.MLS)) {
+ return ConversationProtocol.MLS;
+ }
+
+ if (commonProtocols.includes(ConversationProtocol.PROTEUS)) {
+ return ConversationProtocol.PROTEUS;
+ }
+
+ return null;
+ };
+
+ /**
+ * Fetches a MLS 1:1 conversation between self user and given userId from backend and saves it in both local state and database.
+ *
+ * @param otherUserId - id of the other user
+ * @returns MLS conversation entity
+ */
+ public readonly getMLS1to1Conversation = async (otherUserId: QualifiedId): Promise => {
+ const remoteConversation = await this.conversationService.getMLS1to1Conversation(otherUserId);
+ const [conversationEntity] = this.mapConversations([remoteConversation]);
+
+ const conversation = await this.saveConversation(conversationEntity);
+
+ if (!isMLSConversation(conversation)) {
+ throw new Error('Conversation is not MLS');
+ }
+
+ return conversation;
+ };
+
+ /**
+ * Will establish MLS group for 1:1 conversation or just return it if it's already established.
+ *
+ * @param otherUserId - id of the other user
+ * @param localMLSConversation - local MLS conversation entity - if provided, conversation will not be refetched from the backend
+ * @returns established MLS conversation entity
+ *
+ */
+ private readonly getEstablishedMLS1to1Conversation = async (
+ otherUserId: QualifiedId,
+ localMLSConversation?: MLSConversation,
+ ): Promise => {
+ const mlsService = this.core.service?.mls;
+
+ if (!mlsService) {
+ throw new Error('MLS service is not available!');
+ }
+
+ const mlsConversation = localMLSConversation || (await this.getMLS1to1Conversation(otherUserId));
+ const {groupId} = mlsConversation;
+
+ //check if converstion already exists and if mls group is already established
+
+ const isMLS1to1ConversationEstablished = await this.isMLSConversationEstablished(groupId);
+
+ //if it's already established it's ready to be used - we just return it
+ if (isMLS1to1ConversationEstablished) {
+ return mlsConversation;
+ }
+
+ //if epoch is higher than 0 it means that the group was already established,
+ //if we didn't receive a welcome message, we join with external commit
+ if (mlsConversation.epoch > 0) {
+ await joinNewConversations([mlsConversation], this.core);
+ return mlsConversation;
+ }
+
+ //if it's not established, establish it and add the other user to the group
+ const selfUserId = this.userState.self().qualifiedId;
+ await mlsService.registerConversation(groupId, [selfUserId, otherUserId], {
+ user: selfUserId,
+ client: this.core.clientId,
+ });
+
+ useMLSConversationState.getState().markAsEstablished(groupId);
+ return mlsConversation;
+ };
+
+ private readonly isMLSConversationEstablished = async (groupId: string) => {
+ const mlsService = this.core.service?.mls;
+
+ if (!mlsService) {
+ throw new Error('MLS service is not available!');
+ }
+
+ const mlsConversationState = useMLSConversationState.getState();
+
+ const isMLSConversationMarkedAsEstablished = mlsConversationState.isEstablished(groupId);
+
+ if (isMLSConversationMarkedAsEstablished) {
+ return true;
+ }
+
+ const isMLSConversationEstablished = await mlsService.conversationExists(groupId);
+
+ if (isMLSConversationEstablished) {
+ //make sure MLS group is marked as established
+ mlsConversationState.markAsEstablished(groupId);
+ return true;
+ }
+
+ return false;
+ };
+
+ private readonly getConnectionConversation = async (connectionEntity: ConnectionEntity) => {
+ //before using the default conversation id, check if both users support MLS, if so use MLS 1:1 conversation instead
+ const {conversationId, userId} = connectionEntity;
+
+ const commonProtocol = await this.getProtocolFor1to1Conversation(userId);
const localConversation = this.conversationState.findConversation(conversationId);
+ //if mls is supported by both users, establish a mls conversation
+ if (commonProtocol === ConversationProtocol.MLS) {
+ const localMLSConversation =
+ localConversation && isMLSConversation(localConversation) ? localConversation : undefined;
+
+ return this.getEstablishedMLS1to1Conversation(userId, localMLSConversation);
+ }
+
+ //otherwise use proteus conversation
if (localConversation) {
return localConversation;
}
@@ -1388,6 +1518,7 @@ export class ConversationRepository {
}
conversation.connection(connectionEntity);
+ connectionEntity.conversationId = conversation.qualifiedId;
if (connectionEntity.isConnected()) {
conversation.type(CONVERSATION_TYPE.ONE_TO_ONE);
diff --git a/src/script/conversation/ConversationService.ts b/src/script/conversation/ConversationService.ts
index ed51a62cf25..faeb74ab917 100644
--- a/src/script/conversation/ConversationService.ts
+++ b/src/script/conversation/ConversationService.ts
@@ -430,4 +430,8 @@ export class ConversationService {
async mlsGroupExistsLocally(groupId: string): Promise {
return this.coreConversationService.mlsGroupExistsLocally(groupId);
}
+
+ async getMLS1to1Conversation(userId: QualifiedId) {
+ return this.apiClient.api.conversation.getMLS1to1Conversation(userId);
+ }
}
diff --git a/src/script/main/app.ts b/src/script/main/app.ts
index 398dff87351..f6ffc268d5f 100644
--- a/src/script/main/app.ts
+++ b/src/script/main/app.ts
@@ -250,6 +250,8 @@ export class App {
serverTimeHandler,
);
+ repositories.self = new SelfRepository(selfService, repositories.user, repositories.team, repositories.client);
+
repositories.conversation = new ConversationRepository(
this.service.conversation,
repositories.message,
@@ -257,13 +259,12 @@ export class App {
repositories.event,
repositories.team,
repositories.user,
+ repositories.self,
repositories.properties,
repositories.calling,
serverTimeHandler,
);
- repositories.self = new SelfRepository(selfService, repositories.user, repositories.team, repositories.client);
-
repositories.eventTracker = new EventTrackingRepository(repositories.message);
const serviceMiddleware = new ServiceMiddleware(repositories.conversation, repositories.user);
@@ -413,10 +414,6 @@ export class App {
conversationRepository.initMLSConversationRecoveredListener();
}
- if (connections.length) {
- await Promise.allSettled(conversationRepository.mapConnections(connections));
- }
-
onProgress(25, t('initReceivedUserData'));
telemetry.addStatistic(AppInitStatisticsValue.CONVERSATIONS, conversations.length, 50);
this._subscribeToUnloadEvents(selfUser);
@@ -433,6 +430,10 @@ export class App {
});
const notificationsCount = eventRepository.notificationsTotal;
+ if (connections.length) {
+ await Promise.allSettled(conversationRepository.mapConnections(connections));
+ }
+
if (supportsMLS()) {
// Once all the messages have been processed and the message sending queue freed we can now:
diff --git a/src/script/self/SelfRepository.ts b/src/script/self/SelfRepository.ts
index d7ae6da50a9..32ccce8a28c 100644
--- a/src/script/self/SelfRepository.ts
+++ b/src/script/self/SelfRepository.ts
@@ -131,6 +131,20 @@ export class SelfRepository {
return supportedProtocols;
}
+ public async getSelfSupportedProtocols(): Promise {
+ const selfUser = this.userState.self();
+
+ const localSupportedProtocols = selfUser.supportedProtocols();
+
+ if (localSupportedProtocols) {
+ return localSupportedProtocols;
+ }
+
+ const supportedProtocols = await this.refreshSelfSupportedProtocols();
+
+ return supportedProtocols;
+ }
+
/**
* Update self user's list of supported protocols.
* It will send a request to the backend to change the supported protocols and then update the user in the local state.
diff --git a/src/script/user/UserRepository.ts b/src/script/user/UserRepository.ts
index e0a7f0f42f2..562a3fcd068 100644
--- a/src/script/user/UserRepository.ts
+++ b/src/script/user/UserRepository.ts
@@ -670,6 +670,26 @@ export class UserRepository {
}
}
+ /**
+ * Check for supported protocols on user entity locally, otherwise fetch them from the backend.
+ * @param userId - the user to fetch the supported protocols for
+ */
+
+ public async getUserSupportedProtocols(userId: QualifiedId): Promise {
+ const localUser = this.findUserById(userId);
+
+ if (localUser) {
+ const localSupportedProtocols = localUser.supportedProtocols();
+
+ if (localSupportedProtocols) {
+ return localSupportedProtocols;
+ }
+ }
+
+ const supportedProtocols = await this.userService.getUserSupportedProtocols(userId);
+ return supportedProtocols;
+ }
+
async getUserByHandle(fqn: QualifiedHandle): Promise {
try {
return await this.userService.getUserByFQN(fqn);
@@ -767,10 +787,6 @@ export class UserRepository {
return this.updateUser(userId, {supported_protocols: supportedProtocols});
}
- getSelfSupportedProtocols(): ConversationProtocol[] | null {
- return this.userState.self().supportedProtocols();
- }
-
public async getAllSelfClients() {
return this.clientRepository.getAllSelfClients();
}
diff --git a/src/script/user/UserService.ts b/src/script/user/UserService.ts
index 4cf65f21de8..230a1313dc7 100644
--- a/src/script/user/UserService.ts
+++ b/src/script/user/UserService.ts
@@ -17,6 +17,7 @@
*
*/
+import {ConversationProtocol} from '@wireapp/api-client/lib/conversation';
import type {User as APIClientUser, QualifiedHandle, QualifiedId} from '@wireapp/api-client/lib/user';
import {container} from 'tsyringe';
@@ -148,4 +149,11 @@ export class UserService {
getUser(userId: QualifiedId): Promise {
return this.apiClient.api.user.getUser(userId);
}
+
+ /**
+ * Get the list of user's supported protocols.
+ */
+ getUserSupportedProtocols(userId: QualifiedId): Promise {
+ return this.apiClient.api.user.getUserSupportedProtocols(userId);
+ }
}
From 73fdb41e97e072fb93517b4a51df980d55fd42c6 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 27 Jul 2023 14:53:19 +0900
Subject: [PATCH 06/70] feat: move all conversation events to mls conversation
---
src/__mocks__/@wireapp/core.ts | 1 +
.../ConversationRepository.test.ts | 3 +
.../conversation/ConversationRepository.ts | 37 ++++++------
src/script/conversation/ConversationState.ts | 22 ++++++-
src/script/event/EventService.ts | 58 +++++++++++++++++++
src/script/main/app.ts | 7 +--
test/helper/TestFactory.js | 2 +
7 files changed, 108 insertions(+), 22 deletions(-)
diff --git a/src/__mocks__/@wireapp/core.ts b/src/__mocks__/@wireapp/core.ts
index 533f85edb78..a064774230f 100644
--- a/src/__mocks__/@wireapp/core.ts
+++ b/src/__mocks__/@wireapp/core.ts
@@ -38,6 +38,7 @@ export class Account extends EventEmitter {
conversationExists: jest.fn(),
exportSecretKey: jest.fn(),
leaveConferenceSubconversation: jest.fn(),
+ conversationExists: jest.fn(),
on: this.on,
emit: this.emit,
off: this.off,
diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts
index 5de905b0e1d..3256071cd10 100644
--- a/src/script/conversation/ConversationRepository.test.ts
+++ b/src/script/conversation/ConversationRepository.test.ts
@@ -380,6 +380,7 @@ describe('ConversationRepository', () => {
beforeEach(() => {
connectionEntity = new ConnectionEntity();
connectionEntity.conversationId = conversation_et.qualifiedId;
+ connectionEntity.userId = {id: 'id', domain: 'domain'};
const conversation_payload = {
creator: conversation_et.id,
@@ -402,6 +403,8 @@ describe('ConversationRepository', () => {
} as ConversationDatabaseData;
spyOn(testFactory.conversation_repository as any, 'fetchConversationById').and.callThrough();
+ spyOn(testFactory.user_repository, 'getUserSupportedProtocols').and.returnValue([ConversationProtocol.PROTEUS]);
+ spyOn(testFactory.self_repository, 'getSelfSupportedProtocols').and.returnValue([ConversationProtocol.PROTEUS]);
spyOn(testFactory.conversation_service, 'getConversationById').and.returnValue(
Promise.resolve(conversation_payload),
);
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index bd7c167b238..e9df447848b 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1399,25 +1399,22 @@ export class ConversationRepository {
* Will establish MLS group for 1:1 conversation or just return it if it's already established.
*
* @param otherUserId - id of the other user
- * @param localMLSConversation - local MLS conversation entity - if provided, conversation will not be refetched from the backend
* @returns established MLS conversation entity
*
*/
- private readonly getEstablishedMLS1to1Conversation = async (
- otherUserId: QualifiedId,
- localMLSConversation?: MLSConversation,
- ): Promise => {
+ private readonly getEstablishedMLS1to1Conversation = async (otherUserId: QualifiedId): Promise => {
const mlsService = this.core.service?.mls;
if (!mlsService) {
throw new Error('MLS service is not available!');
}
+ const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
+
const mlsConversation = localMLSConversation || (await this.getMLS1to1Conversation(otherUserId));
const {groupId} = mlsConversation;
//check if converstion already exists and if mls group is already established
-
const isMLS1to1ConversationEstablished = await this.isMLSConversationEstablished(groupId);
//if it's already established it's ready to be used - we just return it
@@ -1425,16 +1422,15 @@ export class ConversationRepository {
return mlsConversation;
}
- //if epoch is higher than 0 it means that the group was already established,
- //if we didn't receive a welcome message, we join with external commit
+ //if epoch is higher that 0 it means that the group is already established but we don't have it in our local state
+ //we have to join with external commit
if (mlsConversation.epoch > 0) {
await joinNewConversations([mlsConversation], this.core);
- return mlsConversation;
}
//if it's not established, establish it and add the other user to the group
const selfUserId = this.userState.self().qualifiedId;
- await mlsService.registerConversation(groupId, [selfUserId, otherUserId], {
+ await mlsService.registerConversation(groupId, [otherUserId, selfUserId], {
user: selfUserId,
client: this.core.clientId,
});
@@ -1473,21 +1469,28 @@ export class ConversationRepository {
//before using the default conversation id, check if both users support MLS, if so use MLS 1:1 conversation instead
const {conversationId, userId} = connectionEntity;
- const commonProtocol = await this.getProtocolFor1to1Conversation(userId);
+ const proteusConversation = this.conversationState.findConversation(conversationId);
- const localConversation = this.conversationState.findConversation(conversationId);
+ const commonProtocol = await this.getProtocolFor1to1Conversation(userId);
//if mls is supported by both users, establish a mls conversation
if (commonProtocol === ConversationProtocol.MLS) {
- const localMLSConversation =
- localConversation && isMLSConversation(localConversation) ? localConversation : undefined;
+ const establishedMLS1to1Conversation = await this.getEstablishedMLS1to1Conversation(userId);
+
+ //move events to new conversation and hide old proteus conversation
+ await this.eventService.moveEventsToConversation(conversationId.id, establishedMLS1to1Conversation.id);
+
+ //if proteus conversation is known locally, clear it so it does not appear in the UI
+ if (proteusConversation) {
+ await this.clearConversation(proteusConversation);
+ }
- return this.getEstablishedMLS1to1Conversation(userId, localMLSConversation);
+ return establishedMLS1to1Conversation;
}
//otherwise use proteus conversation
- if (localConversation) {
- return localConversation;
+ if (proteusConversation) {
+ return proteusConversation;
}
if (connectionEntity.isConnected() || connectionEntity.isOutgoingRequest()) {
diff --git a/src/script/conversation/ConversationState.ts b/src/script/conversation/ConversationState.ts
index 487d215b0d2..37569af88cc 100644
--- a/src/script/conversation/ConversationState.ts
+++ b/src/script/conversation/ConversationState.ts
@@ -18,6 +18,7 @@
*/
import {ConnectionStatus} from '@wireapp/api-client/lib/connection/';
+import {CONVERSATION_TYPE} from '@wireapp/api-client/lib/conversation';
import {QualifiedId} from '@wireapp/api-client/lib/user';
import ko from 'knockout';
import {container, singleton} from 'tsyringe';
@@ -25,7 +26,7 @@ import {container, singleton} from 'tsyringe';
import {matchQualifiedIds} from 'Util/QualifiedId';
import {sortGroupsByLastEvent} from 'Util/util';
-import {isMLSConversation, isSelfConversation} from './ConversationSelectors';
+import {MLSConversation, isMLSConversation, isSelfConversation} from './ConversationSelectors';
import {Conversation} from '../entity/Conversation';
import {User} from '../entity/User';
@@ -192,6 +193,25 @@ export class ConversationState {
});
}
+ /**
+ * Find a local MLS 1:1 conversation by user Id.
+ * @returns Conversation is locally available
+ */
+ findMLS1to1Conversation(userId: QualifiedId): MLSConversation | undefined {
+ return this.conversations().find((conversation): conversation is MLSConversation => {
+ const conversationMembersIds = conversation.participating_user_ids();
+ const otherUserId = conversationMembersIds[0];
+
+ return (
+ isMLSConversation(conversation) &&
+ conversation.type() === CONVERSATION_TYPE.ONE_TO_ONE &&
+ conversationMembersIds.length === 1 &&
+ otherUserId &&
+ otherUserId.id === userId.id
+ );
+ });
+ }
+
isSelfConversation(conversationId: QualifiedId): boolean {
const selfConversationIds: QualifiedId[] = [this.selfProteusConversation(), this.selfMLSConversation()]
.filter((conversation): conversation is Conversation => !!conversation)
diff --git a/src/script/event/EventService.ts b/src/script/event/EventService.ts
index f605164846a..3d317488109 100644
--- a/src/script/event/EventService.ts
+++ b/src/script/event/EventService.ts
@@ -17,6 +17,7 @@
*
*/
+import {CONVERSATION_EVENT} from '@wireapp/api-client/lib/event/';
import type {Dexie} from 'dexie';
import {container} from 'tsyringe';
@@ -24,6 +25,8 @@ import {Asset as ProtobufAsset} from '@wireapp/protocol-messaging';
import {getLogger, Logger} from 'Util/Logger';
+import {CONVERSATION as CLIENT_CONVERSATION_EVENT} from './Client';
+
import {AssetTransferState} from '../assets/AssetTransferState';
import {BaseError, BASE_ERROR_TYPE} from '../error/BaseError';
import {ConversationError} from '../error/ConversationError';
@@ -518,4 +521,59 @@ export class EventService {
async deleteEvents(conversationId: string, isoDate?: string): Promise {
return this.storageService.deleteEventsByDate(StorageSchemata.OBJECT_STORE.EVENTS, conversationId, isoDate);
}
+
+ /**
+ * Will move all the events from one conversation to another.
+ * Events will get moved, not copied, so all the events from the source conversation will be deleted.
+ * This is used when MLS 1:1 conversation is established and we need to move the events from proteus conversation.
+ *
+ * @param conversationId - conversation id from which events should be moved
+ * @param newConversationId - conversation id to which events should be moved
+ */
+ public async moveEventsToConversation(conversationId: string, newConversationId: string) {
+ const eventsToSkip = [CLIENT_CONVERSATION_EVENT.ONE2ONE_CREATION];
+
+ const events = await this.loadAllConversationEvents(conversationId, eventsToSkip);
+
+ const eventsToMove = events.map(event => ({
+ ...event,
+ conversation: newConversationId,
+ }));
+
+ return Promise.all(
+ eventsToMove.map(event => {
+ return this.storageService.save(StorageSchemata.OBJECT_STORE.EVENTS, event.primary_key, event);
+ }),
+ );
+ }
+
+ /**
+ *
+ * @param conversationId The conversation ID
+ */
+ async loadAllConversationEvents(
+ conversationId: string,
+ eventTypesToSkip: (CONVERSATION_EVENT | CLIENT_CONVERSATION_EVENT)[],
+ ): Promise {
+ try {
+ if (this.storageService.db) {
+ const events = await this.storageService.db
+ .table(StorageSchemata.OBJECT_STORE.EVENTS)
+ .where('conversation')
+ .equals(conversationId)
+ .and(record => !eventTypesToSkip.includes(record.type))
+ .toArray();
+ return events;
+ }
+
+ const records = await this.storageService.getAll(StorageSchemata.OBJECT_STORE.EVENTS);
+ return records.filter(
+ record => record.conversation === conversationId && !eventTypesToSkip.includes(record.type),
+ );
+ } catch (error) {
+ const logMessage = `Failed to get events for conversation '${conversationId}': ${error.message}`;
+ this.logger.error(logMessage, error);
+ throw error;
+ }
+ }
}
diff --git a/src/script/main/app.ts b/src/script/main/app.ts
index f6ffc268d5f..2fd5a1ab07d 100644
--- a/src/script/main/app.ts
+++ b/src/script/main/app.ts
@@ -413,6 +413,9 @@ export class App {
await initMLSCallbacks(this.core, this.repository.conversation);
conversationRepository.initMLSConversationRecoveredListener();
}
+ if (supportsSelfSupportedProtocolsUpdates()) {
+ await selfRepository.initialisePeriodicSelfSupportedProtocolsCheck();
+ }
onProgress(25, t('initReceivedUserData'));
telemetry.addStatistic(AppInitStatisticsValue.CONVERSATIONS, conversations.length, 50);
@@ -462,10 +465,6 @@ export class App {
await conversationRepository.updateConversationsOnAppInit();
await conversationRepository.conversationLabelRepository.loadLabels();
- if (supportsSelfSupportedProtocolsUpdates()) {
- await selfRepository.initialisePeriodicSelfSupportedProtocolsCheck();
- }
-
amplify.publish(WebAppEvents.LIFECYCLE.LOADED);
telemetry.timeStep(AppInitTimingsStep.UPDATED_CONVERSATIONS);
diff --git a/test/helper/TestFactory.js b/test/helper/TestFactory.js
index 82c3f8ec671..28d3b42a16b 100644
--- a/test/helper/TestFactory.js
+++ b/test/helper/TestFactory.js
@@ -259,6 +259,7 @@ export class TestFactory {
await this.exposeConnectionActors();
await this.exposeTeamActors();
await this.exposeEventActors();
+ await this.exposeSelfActors();
this.conversation_service = new ConversationService(this.event_service);
@@ -297,6 +298,7 @@ export class TestFactory {
this.event_repository,
this.team_repository,
this.user_repository,
+ this.self_repository,
this.propertyRepository,
this.calling_repository,
serverTimeHandler,
From c4f110bf18a0c9660f101ed2015ac221434f83be Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Fri, 28 Jul 2023 14:25:57 +0900
Subject: [PATCH 07/70] feat: replace proteus 1:1 with mls 1:1
---
.../conversation/ConversationRepository.ts | 46 ++++++++++++++-----
src/script/user/UserRepository.ts | 13 ++----
2 files changed, 37 insertions(+), 22 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index e9df447848b..d9b7ecfabd9 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1409,9 +1409,7 @@ export class ConversationRepository {
throw new Error('MLS service is not available!');
}
- const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
-
- const mlsConversation = localMLSConversation || (await this.getMLS1to1Conversation(otherUserId));
+ const mlsConversation = await this.getMLS1to1Conversation(otherUserId);
const {groupId} = mlsConversation;
//check if converstion already exists and if mls group is already established
@@ -1465,24 +1463,48 @@ export class ConversationRepository {
return false;
};
+ private readonly replace1to1ProteusConversationWithMS = async (
+ proteusConversation: Conversation,
+ mlsConversation: MLSConversation,
+ ) => {
+ //move events to new conversation and hide old proteus conversation
+ await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
+
+ //if proteus conversation is known locally, clear it so it does not appear in the UI
+ await this.clearConversation(proteusConversation);
+ };
+
private readonly getConnectionConversation = async (connectionEntity: ConnectionEntity) => {
- //before using the default conversation id, check if both users support MLS, if so use MLS 1:1 conversation instead
- const {conversationId, userId} = connectionEntity;
+ //As of how backed works now (August 2023), proteus 1:1 conversations will always be created, even if both users support MLS conversation.
+ //Proteus 1:1 conversation is created right after a connection request is sent.
+ //Therefore, conversationId filed on connectionEntity will always indicate proteus 1:1 conversation.
+ //We need to manually check if both users support MLS conversation and if so, create (or use if it exists already) a MLS 1:1 conversation.
+ const {conversationId, userId} = connectionEntity;
const proteusConversation = this.conversationState.findConversation(conversationId);
+ //if mls 1:1 exists and is already established, nothing more to do, just use it
+ const localMLSConversation = this.conversationState.findMLS1to1Conversation(userId);
+
+ const isMLS1to1ConversationAlreadyEstablishedML =
+ localMLSConversation && (await this.isMLSConversationEstablished(localMLSConversation.groupId));
+
+ if (isMLS1to1ConversationAlreadyEstablishedML) {
+ if (proteusConversation) {
+ await this.replace1to1ProteusConversationWithMS(proteusConversation, localMLSConversation);
+ }
+ return localMLSConversation;
+ }
+
+ //otherwise check common protocol
const commonProtocol = await this.getProtocolFor1to1Conversation(userId);
//if mls is supported by both users, establish a mls conversation
if (commonProtocol === ConversationProtocol.MLS) {
const establishedMLS1to1Conversation = await this.getEstablishedMLS1to1Conversation(userId);
- //move events to new conversation and hide old proteus conversation
- await this.eventService.moveEventsToConversation(conversationId.id, establishedMLS1to1Conversation.id);
-
- //if proteus conversation is known locally, clear it so it does not appear in the UI
if (proteusConversation) {
- await this.clearConversation(proteusConversation);
+ await this.replace1to1ProteusConversationWithMS(proteusConversation, establishedMLS1to1Conversation);
}
return establishedMLS1to1Conversation;
@@ -2186,7 +2208,7 @@ export class ConversationRepository {
* @param timestamp Optional timestamps for which messages to remove
*/
private async _clearConversation(conversationEntity: Conversation, timestamp?: number) {
- this.deleteMessages(conversationEntity, timestamp);
+ await this.deleteMessages(conversationEntity, timestamp);
if (conversationEntity.removed_from_conversation()) {
await this.conversationService.deleteConversationFromDb(conversationEntity.id);
@@ -3515,7 +3537,7 @@ export class ConversationRepository {
conversationEntity.hasCreationMessage = false;
const iso_date = timestamp ? new Date(timestamp).toISOString() : undefined;
- this.eventService.deleteEvents(conversationEntity.id, iso_date);
+ return this.eventService.deleteEvents(conversationEntity.id, iso_date);
}
/**
diff --git a/src/script/user/UserRepository.ts b/src/script/user/UserRepository.ts
index 562a3fcd068..0f90fa620a8 100644
--- a/src/script/user/UserRepository.ts
+++ b/src/script/user/UserRepository.ts
@@ -676,17 +676,10 @@ export class UserRepository {
*/
public async getUserSupportedProtocols(userId: QualifiedId): Promise {
- const localUser = this.findUserById(userId);
-
- if (localUser) {
- const localSupportedProtocols = localUser.supportedProtocols();
-
- if (localSupportedProtocols) {
- return localSupportedProtocols;
- }
- }
-
const supportedProtocols = await this.userService.getUserSupportedProtocols(userId);
+
+ //update local user entity with new supported protocols
+ await this.updateUserSupportedProtocols(userId, supportedProtocols);
return supportedProtocols;
}
From b96f6b10e35d6d2dfb81cdb616efdc7530249032 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 31 Jul 2023 12:09:50 +0900
Subject: [PATCH 08/70] feat: delete proteus 1:1 after moving events to mls 1:1
---
src/script/conversation/ConversationRepository.ts | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index d9b7ecfabd9..7d2737d30cc 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1463,22 +1463,23 @@ export class ConversationRepository {
return false;
};
- private readonly replace1to1ProteusConversationWithMS = async (
+ private readonly replaceProteus1to1ConversationWithMS = async (
proteusConversation: Conversation,
mlsConversation: MLSConversation,
) => {
//move events to new conversation and hide old proteus conversation
await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
- //if proteus conversation is known locally, clear it so it does not appear in the UI
- await this.clearConversation(proteusConversation);
+ //delete conversation locally so it does not appear in the ui
+ await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
};
private readonly getConnectionConversation = async (connectionEntity: ConnectionEntity) => {
//As of how backed works now (August 2023), proteus 1:1 conversations will always be created, even if both users support MLS conversation.
//Proteus 1:1 conversation is created right after a connection request is sent.
//Therefore, conversationId filed on connectionEntity will always indicate proteus 1:1 conversation.
- //We need to manually check if both users support MLS conversation and if so, create (or use if it exists already) a MLS 1:1 conversation.
+ //We need to manually check if mls 1:1 conversation can be used instead.
+ //When both users support MLS conversation and connection is accepted create (or use if it exists already) a MLS 1:1 conversation.
const {conversationId, userId} = connectionEntity;
const proteusConversation = this.conversationState.findConversation(conversationId);
@@ -1491,7 +1492,7 @@ export class ConversationRepository {
if (isMLS1to1ConversationAlreadyEstablishedML) {
if (proteusConversation) {
- await this.replace1to1ProteusConversationWithMS(proteusConversation, localMLSConversation);
+ await this.replaceProteus1to1ConversationWithMS(proteusConversation, localMLSConversation);
}
return localMLSConversation;
}
@@ -1504,7 +1505,7 @@ export class ConversationRepository {
const establishedMLS1to1Conversation = await this.getEstablishedMLS1to1Conversation(userId);
if (proteusConversation) {
- await this.replace1to1ProteusConversationWithMS(proteusConversation, establishedMLS1to1Conversation);
+ await this.replaceProteus1to1ConversationWithMS(proteusConversation, establishedMLS1to1Conversation);
}
return establishedMLS1to1Conversation;
From 26369e7376188777c2dad95e1955a5d1c1fccff5 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Wed, 2 Aug 2023 10:25:05 +0900
Subject: [PATCH 09/70] runfix: fetch all known users on app load to get fresh
supported protocols
---
src/script/user/UserRepository.ts | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/src/script/user/UserRepository.ts b/src/script/user/UserRepository.ts
index 0f90fa620a8..d9619bfc8de 100644
--- a/src/script/user/UserRepository.ts
+++ b/src/script/user/UserRepository.ts
@@ -227,14 +227,10 @@ export class UserRepository {
await this.userService.removeUserFromDb(orphanUser.qualified_id);
}
- const missingUsers = users.filter(
- user =>
- // The self user doesn't need to be re-fetched
- !matchQualifiedIds(selfUser.qualifiedId, user) &&
- !liveUsers.find(localUser => matchQualifiedIds(user, localUser.qualified_id)),
- );
+ // The self user doesn't need to be re-fetched
+ const usersToFetch = users.filter(user => !matchQualifiedIds(selfUser.qualifiedId, user));
- const {found, failed} = await this.fetchRawUsers(missingUsers, selfUser.domain);
+ const {found, failed} = await this.fetchRawUsers(usersToFetch, selfUser.domain);
const userWithAvailability = found.map(user => {
const availability = incompleteUsers
@@ -676,6 +672,12 @@ export class UserRepository {
*/
public async getUserSupportedProtocols(userId: QualifiedId): Promise {
+ const localSupportedProtocols = this.findUserById(userId)?.supportedProtocols();
+
+ if (localSupportedProtocols) {
+ return localSupportedProtocols;
+ }
+
const supportedProtocols = await this.userService.getUserSupportedProtocols(userId);
//update local user entity with new supported protocols
From fd909318785c8d125397ee910055d50aed694f19 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Wed, 2 Aug 2023 15:51:04 +0900
Subject: [PATCH 10/70] feat: establish mls group when both users support mls
---
.env.localhost | 1 +
server/config/client.config.ts | 1 +
server/config/env.ts | 3 +
.../conversation/ConversationRepository.ts | 272 ++++++++++++------
src/script/util/util.ts | 3 +
5 files changed, 199 insertions(+), 81 deletions(-)
diff --git a/.env.localhost b/.env.localhost
index dee76fb703c..9ea0c43e8a2 100644
--- a/.env.localhost
+++ b/.env.localhost
@@ -21,6 +21,7 @@ ENABLE_DEV_BACKEND_API="true"
#FEATURE_APPLOCK_UNFOCUS_TIMEOUT="30"
#FEATURE_APPLOCK_SCHEDULED_TIMEOUT="30"
#FEATURE_ENABLE_MLS="true"
+#FEATURE_ENABLE_MLS_1_TO_1_CONVERSATIONS="true"
#FEATURE_ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES="true"
#FEATURE_USE_CORE_CRYPTO="true"
diff --git a/server/config/client.config.ts b/server/config/client.config.ts
index 71756cee5e2..8e2b0fbecca 100644
--- a/server/config/client.config.ts
+++ b/server/config/client.config.ts
@@ -55,6 +55,7 @@ export function generateConfig(params: ConfigGeneratorParams, env: Env) {
ENABLE_EXTRA_CLIENT_ENTROPY: env.FEATURE_ENABLE_EXTRA_CLIENT_ENTROPY == 'true',
ENABLE_MEDIA_EMBEDS: env.FEATURE_ENABLE_MEDIA_EMBEDS != 'false',
ENABLE_MLS: env.FEATURE_ENABLE_MLS == 'true',
+ ENABLE_MLS_1_TO_1_CONVERSATIONS: env.FEATURE_ENABLE_MLS_1_TO_1_CONVERSATIONS == 'true',
ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES: env.FEATURE_ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES == 'true',
ENABLE_PHONE_LOGIN: env.FEATURE_ENABLE_PHONE_LOGIN != 'false',
ENABLE_PROTEUS_CORE_CRYPTO: env.FEATURE_ENABLE_PROTEUS_CORE_CRYPTO == 'true',
diff --git a/server/config/env.ts b/server/config/env.ts
index 765f646cd11..83461664188 100644
--- a/server/config/env.ts
+++ b/server/config/env.ts
@@ -80,6 +80,9 @@ export type Env = {
/** will enable the MLS protocol */
FEATURE_ENABLE_MLS?: string;
+ /** will enable the MLS protocol for 1:1 conversations */
+ FEATURE_ENABLE_MLS_1_TO_1_CONVERSATIONS?: string;
+
/** will enable the user to periodically update the list of supported protocols */
FEATURE_ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES?: string;
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 7d2737d30cc..6609a9c23df 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -70,6 +70,7 @@ import {
} from 'Util/StringUtil';
import {TIME_IN_MILLIS} from 'Util/TimeUtil';
import {isBackendError} from 'Util/TypePredicateUtil';
+import {supportsMLS1To1Conversations} from 'Util/util';
import {createUuid} from 'Util/uuid';
import {ACCESS_STATE} from './AccessState';
@@ -711,9 +712,6 @@ export class ConversationRepository {
conversationsData = (await this.conversationService.saveConversationsInDb(data)) as any[];
}
- //TODO: filter out proteus 1:1 conversations that should use 1:1 mls converstions instead
- //when filtering them out change the conversation field of each message to the mls conversation id
-
const allConversationEntities = this.mapConversations(conversationsData);
const newConversationEntities = allConversationEntities.filter(
allConversations =>
@@ -1357,23 +1355,50 @@ export class ConversationRepository {
}
};
- private readonly getProtocolFor1to1Conversation = async (otherUserId: QualifiedId) => {
- const otherUserSupportedProtocolsSet = await this.userRepository.getUserSupportedProtocols(otherUserId);
- const selfUserSupportedProtocolsSet = await this.selfRepository.getSelfSupportedProtocols();
-
- const commonProtocols = otherUserSupportedProtocolsSet.filter(protocol =>
- selfUserSupportedProtocolsSet.includes(protocol),
+ private readonly getProtocolFor1to1Conversation = async (
+ otherUserId: QualifiedId,
+ ): Promise<{
+ protocol: ConversationProtocol.PROTEUS | ConversationProtocol.MLS;
+ isSupportedByTheOtherUser: boolean;
+ }> => {
+ const otherUserSupportedProtocols = await this.userRepository.getUserSupportedProtocols(otherUserId);
+ const selfUserSupportedProtocols = await this.selfRepository.getSelfSupportedProtocols();
+
+ const commonProtocols = otherUserSupportedProtocols.filter(protocol =>
+ selfUserSupportedProtocols.includes(protocol),
);
if (commonProtocols.includes(ConversationProtocol.MLS)) {
- return ConversationProtocol.MLS;
+ return {protocol: ConversationProtocol.MLS, isSupportedByTheOtherUser: true};
}
if (commonProtocols.includes(ConversationProtocol.PROTEUS)) {
- return ConversationProtocol.PROTEUS;
+ return {protocol: ConversationProtocol.PROTEUS, isSupportedByTheOtherUser: true};
+ }
+
+ //if common protocol can't be found, we use preferred protocol of the self user
+ const preferredProtocol = selfUserSupportedProtocols.includes(ConversationProtocol.MLS)
+ ? ConversationProtocol.MLS
+ : ConversationProtocol.PROTEUS;
+
+ return {protocol: preferredProtocol, isSupportedByTheOtherUser: false};
+ };
+
+ /**
+ * Tries to find a MLS 1:1 conversation between self user and given userId in the local state,
+ * otherwise it will try to fetch it from the backend and save it in both memory and database.
+ *
+ * @param otherUserId - id of the other user
+ * @returns MLS conversation entity
+ */
+ private readonly findMLS1to1Conversation = async (otherUserId: QualifiedId): Promise => {
+ const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
+
+ if (localMLSConversation) {
+ return localMLSConversation;
}
- return null;
+ return this.fetchMLS1to1Conversation(otherUserId);
};
/**
@@ -1382,7 +1407,7 @@ export class ConversationRepository {
* @param otherUserId - id of the other user
* @returns MLS conversation entity
*/
- public readonly getMLS1to1Conversation = async (otherUserId: QualifiedId): Promise => {
+ private readonly fetchMLS1to1Conversation = async (otherUserId: QualifiedId): Promise => {
const remoteConversation = await this.conversationService.getMLS1to1Conversation(otherUserId);
const [conversationEntity] = this.mapConversations([remoteConversation]);
@@ -1395,31 +1420,76 @@ export class ConversationRepository {
return conversation;
};
+ private readonly isMLSConversationEstablished = async (groupId: string) => {
+ const mlsService = this.core.service?.mls;
+
+ if (!mlsService) {
+ throw new Error('MLS service is not available!');
+ }
+
+ const mlsConversationState = useMLSConversationState.getState();
+
+ const isMLSConversationMarkedAsEstablished = mlsConversationState.isEstablished(groupId);
+
+ if (isMLSConversationMarkedAsEstablished) {
+ return true;
+ }
+
+ const isMLSConversationEstablished = await mlsService.conversationExists(groupId);
+
+ if (isMLSConversationEstablished) {
+ //make sure MLS group is marked as established
+ mlsConversationState.markAsEstablished(groupId);
+ return true;
+ }
+
+ return false;
+ };
+
/**
- * Will establish MLS group for 1:1 conversation or just return it if it's already established.
+ * Will migrate proteus 1:1 conversation to mls 1:1 conversation.
+ * All the events will be moved to the new conversation and proteus conversation will be deleted locally.
+ * Proteus 1:1 conversation will be hidden in the UI and replaced with mls 1:1 conversation.
*
- * @param otherUserId - id of the other user
- * @returns established MLS conversation entity
+ * @param proteusConversation - proteus 1:1 conversation
+ * @param mlsConversation - mls 1:1 conversation
+ */
+ private readonly replaceProteus1to1WithMLS = async (
+ proteusConversation: Conversation,
+ mlsConversation: MLSConversation,
+ addCreationMessage = false,
+ ) => {
+ //move events to new conversation and hide old proteus conversation
+ await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
+
+ if (addCreationMessage) {
+ this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
+ }
+
+ //TODO: if proteus conversation was active conversation, make sure mls 1:1 is now active (navigate to it)
+ await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
+ };
+
+ /**
+ * Will establish mls 1:1 conversation.
+ * If proteus conversation is provided, it will be replaced with mls 1:1 conversation.
*
+ * @param mlsConversation - mls 1:1 conversation
+ * @param otherUserId - id of the other user
+ * @param proteusConversation - (optional) proteus 1:1 conversation
*/
- private readonly getEstablishedMLS1to1Conversation = async (otherUserId: QualifiedId): Promise => {
+ private readonly establishMLS1to1Conversation = async (
+ mlsConversation: MLSConversation,
+ otherUserId: QualifiedId,
+ ): Promise => {
const mlsService = this.core.service?.mls;
if (!mlsService) {
throw new Error('MLS service is not available!');
}
- const mlsConversation = await this.getMLS1to1Conversation(otherUserId);
const {groupId} = mlsConversation;
- //check if converstion already exists and if mls group is already established
- const isMLS1to1ConversationEstablished = await this.isMLSConversationEstablished(groupId);
-
- //if it's already established it's ready to be used - we just return it
- if (isMLS1to1ConversationEstablished) {
- return mlsConversation;
- }
-
//if epoch is higher that 0 it means that the group is already established but we don't have it in our local state
//we have to join with external commit
if (mlsConversation.epoch > 0) {
@@ -1437,87 +1507,127 @@ export class ConversationRepository {
return mlsConversation;
};
- private readonly isMLSConversationEstablished = async (groupId: string) => {
- const mlsService = this.core.service?.mls;
+ /**
+ * Will initialise mls 1:1 conversation.
+ *
+ * @param mlsConversation - mls 1:1 conversation
+ * @param otherUserId - id of the other user
+ * @param proteusConversation - (optional) proteus 1:1 conversation
+ */
+ private readonly initMLS1to1Conversation = async (
+ mlsConversation: MLSConversation,
+ otherUserId: QualifiedId,
+ isMLSSupportedByTheOtherUser: boolean,
+ proteusConversation?: Conversation,
+ ): Promise => {
+ //if mls is not supported by the other user we have to mark conversation as readonly
+ if (!isMLSSupportedByTheOtherUser) {
+ //TODO: mark conversation as readonly
+ return mlsConversation;
+ }
- if (!mlsService) {
- throw new Error('MLS service is not available!');
+ //if 1:1 conversation is already known locally and is established, its ready to use
+ const isAlreadyEstablished = await this.isMLSConversationEstablished(mlsConversation.groupId);
+
+ if (isAlreadyEstablished) {
+ if (proteusConversation) {
+ await this.replaceProteus1to1WithMLS(proteusConversation, mlsConversation);
+ }
+ return mlsConversation;
}
- const mlsConversationState = useMLSConversationState.getState();
+ const establishedMLSConversation = await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
+ //if it's also supported by the other user we make sure conversation is established and content is migrated
+ if (proteusConversation) {
+ await this.replaceProteus1to1WithMLS(proteusConversation, establishedMLSConversation, true);
+ }
+ return establishedMLSConversation;
+ };
- const isMLSConversationMarkedAsEstablished = mlsConversationState.isEstablished(groupId);
+ public readonly initMLS1to1ConversationForTeam1to1 = async (
+ proteusConversation: Conversation,
+ doesOtherUserSupportMLS: boolean,
+ ) => {
+ const isTeam1to1Conversation = proteusConversation.isTeam1to1();
- if (isMLSConversationMarkedAsEstablished) {
- return true;
+ if (!isTeam1to1Conversation) {
+ throw new Error('Conversation is not team 1:1');
}
- const isMLSConversationEstablished = await mlsService.conversationExists(groupId);
+ const conversationMembersIds = proteusConversation.participating_user_ids();
+ const otherUserId = conversationMembersIds.length === 1 && conversationMembersIds[0];
- if (isMLSConversationEstablished) {
- //make sure MLS group is marked as established
- mlsConversationState.markAsEstablished(groupId);
- return true;
+ if (!otherUserId) {
+ this.logger.error('Could not find other user id in 1:1 conversation');
+ return null;
}
- return false;
+ const mlsConversation = await this.findMLS1to1Conversation(otherUserId);
+ return this.initMLS1to1Conversation(mlsConversation, otherUserId, doesOtherUserSupportMLS, proteusConversation);
};
- private readonly replaceProteus1to1ConversationWithMS = async (
- proteusConversation: Conversation,
- mlsConversation: MLSConversation,
- ) => {
- //move events to new conversation and hide old proteus conversation
- await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
+ private readonly initMLS1to1ConversationForConnection = async (
+ connection: ConnectionEntity,
+ doesOtherUserSupportMLS: boolean,
+ ): Promise => {
+ const {userId: otherUserId, conversationId: proteusConversationId} = connection;
- //delete conversation locally so it does not appear in the ui
- await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
+ const localProteusConversation = this.conversationState.findConversation(proteusConversationId);
+ const mlsConversation = await this.findMLS1to1Conversation(otherUserId);
+
+ //if connection is not yet accepted, we return unestablished mls conversation
+ if (!connection.isConnected()) {
+ return mlsConversation;
+ }
+
+ //if it's also supported by the other user we make sure conversation is established and content is migrated
+ return this.initMLS1to1Conversation(
+ mlsConversation,
+ otherUserId,
+ doesOtherUserSupportMLS,
+ localProteusConversation,
+ );
};
- private readonly getConnectionConversation = async (connectionEntity: ConnectionEntity) => {
- //As of how backed works now (August 2023), proteus 1:1 conversations will always be created, even if both users support MLS conversation.
- //Proteus 1:1 conversation is created right after a connection request is sent.
- //Therefore, conversationId filed on connectionEntity will always indicate proteus 1:1 conversation.
- //We need to manually check if mls 1:1 conversation can be used instead.
- //When both users support MLS conversation and connection is accepted create (or use if it exists already) a MLS 1:1 conversation.
+ private readonly initProteus1to1ConversationForConnection = async (
+ connection: ConnectionEntity,
+ doesOtherUserSupportProteus: boolean,
+ ): Promise => {
+ const {conversationId: proteusConversationId} = connection;
- const {conversationId, userId} = connectionEntity;
- const proteusConversation = this.conversationState.findConversation(conversationId);
+ const localProteusConversation = this.conversationState.findConversation(proteusConversationId);
- //if mls 1:1 exists and is already established, nothing more to do, just use it
- const localMLSConversation = this.conversationState.findMLS1to1Conversation(userId);
+ //if mls is not supported by the other user we have to mark conversation as readonly
- const isMLS1to1ConversationAlreadyEstablishedML =
- localMLSConversation && (await this.isMLSConversationEstablished(localMLSConversation.groupId));
+ const proteusConversation =
+ localProteusConversation ||
+ (connection.isConnected() || connection.isOutgoingRequest()
+ ? await this.fetchConversationById(proteusConversationId)
+ : undefined);
- if (isMLS1to1ConversationAlreadyEstablishedML) {
- if (proteusConversation) {
- await this.replaceProteus1to1ConversationWithMS(proteusConversation, localMLSConversation);
- }
- return localMLSConversation;
+ if (proteusConversation && !doesOtherUserSupportProteus) {
+ //TODO: mark conversation as readonly
}
- //otherwise check common protocol
- const commonProtocol = await this.getProtocolFor1to1Conversation(userId);
-
- //if mls is supported by both users, establish a mls conversation
- if (commonProtocol === ConversationProtocol.MLS) {
- const establishedMLS1to1Conversation = await this.getEstablishedMLS1to1Conversation(userId);
+ return proteusConversation;
+ };
- if (proteusConversation) {
- await this.replaceProteus1to1ConversationWithMS(proteusConversation, establishedMLS1to1Conversation);
- }
+ private readonly getConnectionConversation = async (connectionEntity: ConnectionEntity) => {
+ //As of how backed works now (August 2023), proteus 1:1 conversations will always be created, even if both users support MLS conversation.
+ //Proteus 1:1 conversation is created right after a connection request is sent.
+ //Therefore, conversationId filed on connectionEntity will always indicate proteus 1:1 conversation.
+ //We need to manually check if mls 1:1 conversation can be used instead.
+ //If mls 1:1 conversation is used, proteus 1:1 conversation will be deleted locally.
- return establishedMLS1to1Conversation;
- }
+ const {userId: otherUserId} = connectionEntity;
+ const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(otherUserId);
- //otherwise use proteus conversation
- if (proteusConversation) {
- return proteusConversation;
+ if (protocol === ConversationProtocol.MLS && supportsMLS1To1Conversations()) {
+ return this.initMLS1to1ConversationForConnection(connectionEntity, isSupportedByTheOtherUser);
}
- if (connectionEntity.isConnected() || connectionEntity.isOutgoingRequest()) {
- return this.fetchConversationById(conversationId);
+ if (protocol === ConversationProtocol.PROTEUS) {
+ return this.initProteus1to1ConversationForConnection(connectionEntity, isSupportedByTheOtherUser);
}
return undefined;
diff --git a/src/script/util/util.ts b/src/script/util/util.ts
index 871c2af92a5..1aa250b520e 100644
--- a/src/script/util/util.ts
+++ b/src/script/util/util.ts
@@ -405,6 +405,9 @@ const supportsSecretStorage = () => !Runtime.isDesktopApp() || !!window.systemCr
// disables mls for old 'broken' desktop clients, see https://github.com/wireapp/wire-desktop/pull/6094
export const supportsMLS = () => Config.getConfig().FEATURE.ENABLE_MLS && supportsSecretStorage();
+export const supportsMLS1To1Conversations = () =>
+ supportsMLS() && Config.getConfig().FEATURE.ENABLE_MLS_1_TO_1_CONVERSATIONS;
+
export const supportsSelfSupportedProtocolsUpdates = () =>
Config.getConfig().FEATURE.ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES;
From 249bf0d0da3dd74d0afbe80efea276e74bf93c4c Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Wed, 2 Aug 2023 16:32:56 +0900
Subject: [PATCH 11/70] feat: if proteus 1:1 was active conversation, switch to
mls
---
src/script/conversation/ConversationRepository.ts | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 6609a9c23df..56676da9b52 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1466,6 +1466,12 @@ export class ConversationRepository {
this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
}
+ const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
+
+ if (isActiveConversation) {
+ amplify.publish(WebAppEvents.CONVERSATION.SHOW, mlsConversation, {});
+ }
+
//TODO: if proteus conversation was active conversation, make sure mls 1:1 is now active (navigate to it)
await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
};
@@ -1539,7 +1545,7 @@ export class ConversationRepository {
const establishedMLSConversation = await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
//if it's also supported by the other user we make sure conversation is established and content is migrated
if (proteusConversation) {
- await this.replaceProteus1to1WithMLS(proteusConversation, establishedMLSConversation, true);
+ await this.replaceProteus1to1WithMLS(proteusConversation, establishedMLSConversation);
}
return establishedMLSConversation;
};
From 33428e817d1d793549b64a01bcf8acc0a4dd6133 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Fri, 4 Aug 2023 11:52:10 +0900
Subject: [PATCH 12/70] init 1:1 conversation when navigating between
conversations
---
.../ConversationRepository.test.ts | 5 +-
.../conversation/ConversationRepository.ts | 213 +++++++++++-------
src/script/main/app.ts | 5 +
src/script/user/UserRepository.ts | 5 +-
src/script/view_model/ContentViewModel.ts | 29 ++-
5 files changed, 159 insertions(+), 98 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts
index 3256071cd10..ab4f88efac2 100644
--- a/src/script/conversation/ConversationRepository.test.ts
+++ b/src/script/conversation/ConversationRepository.test.ts
@@ -399,7 +399,7 @@ describe('ConversationRepository', () => {
},
},
name: null,
- type: 0,
+ type: 2,
} as ConversationDatabaseData;
spyOn(testFactory.conversation_repository as any, 'fetchConversationById').and.callThrough();
@@ -411,6 +411,8 @@ describe('ConversationRepository', () => {
});
it('should map a connection to an existing conversation', () => {
+ conversation_et.type(CONVERSATION_TYPE.ONE_TO_ONE);
+
return testFactory.conversation_repository['mapConnection'](connectionEntity).then(
(_conversation: Conversation) => {
expect(testFactory.conversation_repository['fetchConversationById']).not.toHaveBeenCalled();
@@ -432,6 +434,7 @@ describe('ConversationRepository', () => {
});
it('should map a cancelled connection to an existing conversation and filter it', () => {
+ conversation_et.type(CONVERSATION_TYPE.ONE_TO_ONE);
connectionEntity.status(ConnectionStatus.CANCELLED);
return testFactory.conversation_repository['mapConnection'](connectionEntity).then(_conversation => {
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 56676da9b52..de278cbad90 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -85,7 +85,7 @@ import {ConversationFilter} from './ConversationFilter';
import {ConversationLabelRepository} from './ConversationLabelRepository';
import {ConversationDatabaseData, ConversationMapper} from './ConversationMapper';
import {ConversationRoleRepository} from './ConversationRoleRepository';
-import {isMLSConversation, MLSConversation} from './ConversationSelectors';
+import {isMLSConversation, isProteusConversation, MLSConversation, ProteusConversation} from './ConversationSelectors';
import {ConversationService} from './ConversationService';
import {ConversationState} from './ConversationState';
import {ConversationStateHandler} from './ConversationStateHandler';
@@ -1455,24 +1455,34 @@ export class ConversationRepository {
* @param mlsConversation - mls 1:1 conversation
*/
private readonly replaceProteus1to1WithMLS = async (
- proteusConversation: Conversation,
+ proteusConversation: ProteusConversation,
mlsConversation: MLSConversation,
- addCreationMessage = false,
+ shouldMoveEvents = true,
) => {
- //move events to new conversation and hide old proteus conversation
- await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
+ this.logger.info(
+ `Replacing proteus 1:1 conversation ${proteusConversation.id} with mls 1:1 conversation ${mlsConversation.id}`,
+ );
- if (addCreationMessage) {
- this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
+ if (shouldMoveEvents) {
+ this.logger.info('Moving events from proteus 1:1 conversation to MLS 1:1 conversation');
+ await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
}
+ //TODO:
+ // if (proteusConversation.isTeam1to1()) {
+ // this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
+ // }
+
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
if (isActiveConversation) {
+ this.logger.info(
+ `Proteus 1:1 conversation ${proteusConversation.id} is active conversation, showing mls 1:1 conversation ${mlsConversation.id}`,
+ );
amplify.publish(WebAppEvents.CONVERSATION.SHOW, mlsConversation, {});
}
- //TODO: if proteus conversation was active conversation, make sure mls 1:1 is now active (navigate to it)
+ this.logger.info(`Deleting proteus 1:1 conversation ${proteusConversation.id}`);
await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
};
@@ -1500,6 +1510,7 @@ export class ConversationRepository {
//we have to join with external commit
if (mlsConversation.epoch > 0) {
await joinNewConversations([mlsConversation], this.core);
+ return mlsConversation;
}
//if it's not established, establish it and add the other user to the group
@@ -1521,101 +1532,134 @@ export class ConversationRepository {
* @param proteusConversation - (optional) proteus 1:1 conversation
*/
private readonly initMLS1to1Conversation = async (
- mlsConversation: MLSConversation,
+ conversation: Conversation,
otherUserId: QualifiedId,
isMLSSupportedByTheOtherUser: boolean,
- proteusConversation?: Conversation,
+ skipMLSGroupCreation = false,
): Promise => {
- //if mls is not supported by the other user we have to mark conversation as readonly
+ this.logger.info(
+ `Initialising MLS 1:1 conversation ${conversation.id} with user ${otherUserId.id}, provided with ${conversation.protocol} protocol`,
+ );
+ const mlsConversation = isMLSConversation(conversation)
+ ? conversation
+ : await this.findMLS1to1Conversation(otherUserId);
+
+ if (skipMLSGroupCreation) {
+ this.logger.info(
+ `Skipping establishement of MLS group for 1:1 conversation ${conversation.id} with user ${otherUserId.id}.`,
+ );
+ if (isProteusConversation(conversation)) {
+ await this.replaceProteus1to1WithMLS(conversation, mlsConversation, false);
+ }
+ return mlsConversation;
+ }
+
+ //if mls is not supported by the other user we do not establish the group yet
+ //we just mark the mls conversation as readonly and return it
if (!isMLSSupportedByTheOtherUser) {
//TODO: mark conversation as readonly
+ this.logger.info(
+ `MLS 1:1 conversation ${conversation.id} with user ${otherUserId.id} is not supported by the other user, conversation will become readonly`,
+ );
return mlsConversation;
}
- //if 1:1 conversation is already known locally and is established, its ready to use
const isAlreadyEstablished = await this.isMLSConversationEstablished(mlsConversation.groupId);
- if (isAlreadyEstablished) {
- if (proteusConversation) {
- await this.replaceProteus1to1WithMLS(proteusConversation, mlsConversation);
- }
- return mlsConversation;
- }
+ const establishedMLSConversation = isAlreadyEstablished
+ ? mlsConversation
+ : await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
- const establishedMLSConversation = await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
- //if it's also supported by the other user we make sure conversation is established and content is migrated
- if (proteusConversation) {
- await this.replaceProteus1to1WithMLS(proteusConversation, establishedMLSConversation);
+ this.logger.info(`MLS 1:1 conversation ${conversation.id} with user ${otherUserId.id} is established.`);
+ //if proteus conversation was provided, we have to replace it with mls 1:1 conversation
+ if (isProteusConversation(conversation)) {
+ await this.replaceProteus1to1WithMLS(conversation, establishedMLSConversation);
}
+
return establishedMLSConversation;
};
- public readonly initMLS1to1ConversationForTeam1to1 = async (
- proteusConversation: Conversation,
- doesOtherUserSupportMLS: boolean,
- ) => {
- const isTeam1to1Conversation = proteusConversation.isTeam1to1();
+ private readonly initProteus1to1Conversation = async (
+ proteusConversation: ProteusConversation,
+ doesOtherUserSupportProteus: boolean,
+ ): Promise => {
+ //if mls is not supported by the other user we have to mark conversation as readonly
+ if (!doesOtherUserSupportProteus) {
+ //TODO: mark conversation as readonly
+ }
+
+ return proteusConversation;
+ };
- if (!isTeam1to1Conversation) {
- throw new Error('Conversation is not team 1:1');
+ private readonly getUserIdOf1to1Conversation = (conversation: Conversation): QualifiedId | null => {
+ const is1to1Conversation = conversation.is1to1();
+
+ if (!is1to1Conversation) {
+ throw new Error(`Conversation ${conversation.id} is not of type 1:1`);
+ }
+
+ const connection = conversation.connection();
+ const connectionUserId = connection && connection.userId;
+ if (connectionUserId) {
+ return connection.userId;
}
- const conversationMembersIds = proteusConversation.participating_user_ids();
+ const conversationMembersIds = conversation.participating_user_ids();
const otherUserId = conversationMembersIds.length === 1 && conversationMembersIds[0];
- if (!otherUserId) {
- this.logger.error('Could not find other user id in 1:1 conversation');
- return null;
+ if (otherUserId) {
+ return otherUserId;
}
- const mlsConversation = await this.findMLS1to1Conversation(otherUserId);
- return this.initMLS1to1Conversation(mlsConversation, otherUserId, doesOtherUserSupportMLS, proteusConversation);
+ return null;
};
- private readonly initMLS1to1ConversationForConnection = async (
- connection: ConnectionEntity,
- doesOtherUserSupportMLS: boolean,
- ): Promise => {
- const {userId: otherUserId, conversationId: proteusConversationId} = connection;
+ /**
+ * Will initialise 1:1 conversation (either team-owned or regular 1:1)
+ * Will choose the protocol for 1:1 conversation based on the supported protocols of self and the other user.
+ * When both users support mls, mls conversation will be established, content will be moved to mls and proteus conversation will be deleted locally.
+ *
+ * @param conversation - 1:1 conversation to be initialised
+ * @param skipMLSGroupCreation - if true, mls group will not be established for this conversation
+ */
+ public readonly init1to1Conversation = async (
+ conversation: Conversation,
+ skipMLSGroupCreation = false,
+ ): Promise => {
+ if (!conversation.is1to1()) {
+ throw new Error('Conversation is not 1:1');
+ }
- const localProteusConversation = this.conversationState.findConversation(proteusConversationId);
- const mlsConversation = await this.findMLS1to1Conversation(otherUserId);
+ const otherUserId = this.getUserIdOf1to1Conversation(conversation);
- //if connection is not yet accepted, we return unestablished mls conversation
- if (!connection.isConnected()) {
- return mlsConversation;
+ if (!otherUserId) {
+ this.logger.error(`Could not find other user id in 1:1 conversation ${conversation.id}`);
+ return null;
}
- //if it's also supported by the other user we make sure conversation is established and content is migrated
- return this.initMLS1to1Conversation(
- mlsConversation,
- otherUserId,
- doesOtherUserSupportMLS,
- localProteusConversation,
+ this.logger.info(
+ `Initialising 1:1 conversation ${conversation.id} of type ${conversation.type()} with user ${otherUserId.id}`,
);
- };
-
- private readonly initProteus1to1ConversationForConnection = async (
- connection: ConnectionEntity,
- doesOtherUserSupportProteus: boolean,
- ): Promise => {
- const {conversationId: proteusConversationId} = connection;
- const localProteusConversation = this.conversationState.findConversation(proteusConversationId);
+ const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(otherUserId);
+ this.logger.info(
+ `Protocol for 1:1 conversation ${conversation.id} with user ${otherUserId.id} is ${protocol}, isSupportedByTheOtherUser: ${isSupportedByTheOtherUser}`,
+ );
- //if mls is not supported by the other user we have to mark conversation as readonly
+ const shouldUseMLSProtocol = protocol === ConversationProtocol.MLS && supportsMLS1To1Conversations();
- const proteusConversation =
- localProteusConversation ||
- (connection.isConnected() || connection.isOutgoingRequest()
- ? await this.fetchConversationById(proteusConversationId)
- : undefined);
+ //if both users support mls or mls conversation is already established, we use it
+ //we never go back to proteus conversation, even if one of the users do not support mls anymore
+ //(e.g. due to the change of supported protocols in team configuration)
+ if (shouldUseMLSProtocol || isMLSConversation(conversation)) {
+ return this.initMLS1to1Conversation(conversation, otherUserId, isSupportedByTheOtherUser, skipMLSGroupCreation);
+ }
- if (proteusConversation && !doesOtherUserSupportProteus) {
- //TODO: mark conversation as readonly
+ if (protocol === ConversationProtocol.PROTEUS && isProteusConversation(conversation)) {
+ return this.initProteus1to1Conversation(conversation, isSupportedByTheOtherUser);
}
- return proteusConversation;
+ return null;
};
private readonly getConnectionConversation = async (connectionEntity: ConnectionEntity) => {
@@ -1625,18 +1669,27 @@ export class ConversationRepository {
//We need to manually check if mls 1:1 conversation can be used instead.
//If mls 1:1 conversation is used, proteus 1:1 conversation will be deleted locally.
- const {userId: otherUserId} = connectionEntity;
- const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(otherUserId);
+ const {conversationId} = connectionEntity;
+
+ const localConversation = this.conversationState.findConversation(conversationId);
+
+ const isConnectionAccepted = connectionEntity.isConnected();
+
+ const shouldFetchConversation = isConnectionAccepted || connectionEntity.isOutgoingRequest();
+
+ const conversation =
+ localConversation || (shouldFetchConversation ? await this.fetchConversationById(conversationId) : undefined);
- if (protocol === ConversationProtocol.MLS && supportsMLS1To1Conversations()) {
- return this.initMLS1to1ConversationForConnection(connectionEntity, isSupportedByTheOtherUser);
+ if (!conversation) {
+ return undefined;
}
- if (protocol === ConversationProtocol.PROTEUS) {
- return this.initProteus1to1ConversationForConnection(connectionEntity, isSupportedByTheOtherUser);
+ if (isConnectionAccepted) {
+ conversation.type(CONVERSATION_TYPE.ONE_TO_ONE);
+ return this.init1to1Conversation(conversation);
}
- return undefined;
+ return conversation;
};
/**
@@ -1659,18 +1712,14 @@ export class ConversationRepository {
return undefined;
}
- conversation.connection(connectionEntity);
connectionEntity.conversationId = conversation.qualifiedId;
+ conversation.connection(connectionEntity);
- if (connectionEntity.isConnected()) {
- conversation.type(CONVERSATION_TYPE.ONE_TO_ONE);
- }
-
- const updatedConversation = await this.updateParticipatingUserEntities(conversation);
+ await this.updateParticipatingUserEntities(conversation);
this.conversationState.conversations.notifySubscribers();
- return updatedConversation;
+ return conversation;
} catch (error) {
const isConversationNotFound =
error instanceof ConversationError && error.type === ConversationError.TYPE.CONVERSATION_NOT_FOUND;
diff --git a/src/script/main/app.ts b/src/script/main/app.ts
index 2fd5a1ab07d..49e62f6fcf8 100644
--- a/src/script/main/app.ts
+++ b/src/script/main/app.ts
@@ -433,6 +433,11 @@ export class App {
});
const notificationsCount = eventRepository.notificationsTotal;
+ const conversations1to1 = conversations.filter(conversation => conversation.is1to1());
+ await Promise.allSettled(
+ conversations1to1.map(conversation => conversationRepository.init1to1Conversation(conversation)),
+ );
+
if (connections.length) {
await Promise.allSettled(conversationRepository.mapConnections(connections));
}
diff --git a/src/script/user/UserRepository.ts b/src/script/user/UserRepository.ts
index d9619bfc8de..2ba3f82b46b 100644
--- a/src/script/user/UserRepository.ts
+++ b/src/script/user/UserRepository.ts
@@ -217,8 +217,7 @@ export class UserRepository {
const [localUsers, incompleteUsers] = partition(dbUsers, user => !!user.qualified_id);
/** users we have in the DB that are not matching any loaded users */
- const [orphanUsers, liveUsers] = partition(
- localUsers,
+ const orphanUsers = localUsers.filter(
localUser => !users.find(user => matchQualifiedIds(user, localUser.qualified_id)),
);
@@ -246,7 +245,7 @@ export class UserRepository {
// Save all new users to the database
await Promise.all(userWithAvailability.map(user => this.saveUserInDb(user)));
- const mappedUsers = this.mapUserResponse(userWithAvailability.concat(liveUsers), failed);
+ const mappedUsers = this.mapUserResponse(userWithAvailability, failed);
// Assign connections to users
mappedUsers.forEach(user => {
diff --git a/src/script/view_model/ContentViewModel.ts b/src/script/view_model/ContentViewModel.ts
index f6b0ddf8684..e165cb9dd4f 100644
--- a/src/script/view_model/ContentViewModel.ts
+++ b/src/script/view_model/ContentViewModel.ts
@@ -180,18 +180,22 @@ export class ContentViewModel {
);
}
- const isActiveConversation = this.conversationState.isActiveConversation(conversationEntity);
+ const initialisedConversation =
+ (conversationEntity.is1to1() && (await this.conversationRepository.init1to1Conversation(conversationEntity))) ||
+ conversationEntity;
+
+ const isActiveConversation = this.conversationState.isActiveConversation(initialisedConversation);
if (!isActiveConversation) {
rightSidebar.close();
}
const isConversationState = contentState === ContentState.CONVERSATION;
- const isOpenedConversation = conversationEntity && isActiveConversation && isConversationState;
+ const isOpenedConversation = initialisedConversation && isActiveConversation && isConversationState;
if (isOpenedConversation) {
if (openNotificationSettings) {
- rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: conversationEntity});
+ rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: initialisedConversation});
}
return;
}
@@ -200,25 +204,26 @@ export class ContentViewModel {
this.mainViewModel.list.openConversations();
if (!isActiveConversation) {
- this.conversationState.activeConversation(conversationEntity);
+ this.conversationState.activeConversation(initialisedConversation);
}
- const messageEntity = openFirstSelfMention ? conversationEntity.getFirstUnreadSelfMention() : exposeMessageEntity;
+ const messageEntity = openFirstSelfMention
+ ? initialisedConversation.getFirstUnreadSelfMention()
+ : exposeMessageEntity;
- if (conversationEntity.is_cleared()) {
- conversationEntity.cleared_timestamp(0);
+ if (initialisedConversation.is_cleared()) {
+ initialisedConversation.cleared_timestamp(0);
}
- if (conversationEntity.is_archived()) {
- await this.conversationRepository.unarchiveConversation(conversationEntity);
+ if (initialisedConversation.is_archived()) {
+ await this.conversationRepository.unarchiveConversation(initialisedConversation);
}
- this.changeConversation(conversationEntity, messageEntity);
+ this.changeConversation(initialisedConversation, messageEntity);
this.showContent(ContentState.CONVERSATION);
this.previousConversation = this.conversationState.activeConversation();
setHistoryParam(
- generateConversationUrl({id: conversationEntity?.id ?? '', domain: conversationEntity?.domain ?? ''}),
- history.state,
+ generateConversationUrl({id: initialisedConversation?.id ?? '', domain: initialisedConversation?.domain ?? ''}),
);
if (openNotificationSettings) {
From 1cb5700fb481d507da28f3ff2f5c1f5f8d517356 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 7 Aug 2023 14:27:07 +0900
Subject: [PATCH 13/70] runfix: always use mls 1:1 if its already known by
client
---
.../conversation/ConversationRepository.ts | 102 ++++++++++--------
src/script/main/app.ts | 5 -
2 files changed, 56 insertions(+), 51 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index de278cbad90..7396d98d3e6 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1532,34 +1532,18 @@ export class ConversationRepository {
* @param proteusConversation - (optional) proteus 1:1 conversation
*/
private readonly initMLS1to1Conversation = async (
- conversation: Conversation,
otherUserId: QualifiedId,
isMLSSupportedByTheOtherUser: boolean,
- skipMLSGroupCreation = false,
): Promise => {
- this.logger.info(
- `Initialising MLS 1:1 conversation ${conversation.id} with user ${otherUserId.id}, provided with ${conversation.protocol} protocol`,
- );
- const mlsConversation = isMLSConversation(conversation)
- ? conversation
- : await this.findMLS1to1Conversation(otherUserId);
-
- if (skipMLSGroupCreation) {
- this.logger.info(
- `Skipping establishement of MLS group for 1:1 conversation ${conversation.id} with user ${otherUserId.id}.`,
- );
- if (isProteusConversation(conversation)) {
- await this.replaceProteus1to1WithMLS(conversation, mlsConversation, false);
- }
- return mlsConversation;
- }
+ this.logger.info(`Initialising MLS 1:1 conversation with user ${otherUserId.id}...`);
+ const mlsConversation = await this.findMLS1to1Conversation(otherUserId);
//if mls is not supported by the other user we do not establish the group yet
//we just mark the mls conversation as readonly and return it
if (!isMLSSupportedByTheOtherUser) {
//TODO: mark conversation as readonly
this.logger.info(
- `MLS 1:1 conversation ${conversation.id} with user ${otherUserId.id} is not supported by the other user, conversation will become readonly`,
+ `MLS 1:1 conversation with user ${otherUserId.id} is not supported by the other user, conversation will become readonly`,
);
return mlsConversation;
}
@@ -1570,19 +1554,21 @@ export class ConversationRepository {
? mlsConversation
: await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
- this.logger.info(`MLS 1:1 conversation ${conversation.id} with user ${otherUserId.id} is established.`);
- //if proteus conversation was provided, we have to replace it with mls 1:1 conversation
- if (isProteusConversation(conversation)) {
- await this.replaceProteus1to1WithMLS(conversation, establishedMLSConversation);
- }
-
+ this.logger.info(`MLS 1:1 conversation with user ${otherUserId.id} is established.`);
return establishedMLSConversation;
};
private readonly initProteus1to1Conversation = async (
- proteusConversation: ProteusConversation,
+ proteusConversationId: QualifiedId,
doesOtherUserSupportProteus: boolean,
- ): Promise => {
+ ): Promise => {
+ const localProteusConversation = this.conversationState.findConversation(proteusConversationId);
+ const proteusConversation = localProteusConversation || (await this.fetchConversationById(proteusConversationId));
+
+ if (!isProteusConversation(proteusConversation)) {
+ throw new Error('initProteus1to1Conversation provided with conversation id of conversation that is not proteus');
+ }
+
//if mls is not supported by the other user we have to mark conversation as readonly
if (!doesOtherUserSupportProteus) {
//TODO: mark conversation as readonly
@@ -1622,10 +1608,7 @@ export class ConversationRepository {
* @param conversation - 1:1 conversation to be initialised
* @param skipMLSGroupCreation - if true, mls group will not be established for this conversation
*/
- public readonly init1to1Conversation = async (
- conversation: Conversation,
- skipMLSGroupCreation = false,
- ): Promise => {
+ public readonly init1to1Conversation = async (conversation: Conversation): Promise => {
if (!conversation.is1to1()) {
throw new Error('Conversation is not 1:1');
}
@@ -1652,7 +1635,7 @@ export class ConversationRepository {
//we never go back to proteus conversation, even if one of the users do not support mls anymore
//(e.g. due to the change of supported protocols in team configuration)
if (shouldUseMLSProtocol || isMLSConversation(conversation)) {
- return this.initMLS1to1Conversation(conversation, otherUserId, isSupportedByTheOtherUser, skipMLSGroupCreation);
+ return this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
}
if (protocol === ConversationProtocol.PROTEUS && isProteusConversation(conversation)) {
@@ -1669,27 +1652,51 @@ export class ConversationRepository {
//We need to manually check if mls 1:1 conversation can be used instead.
//If mls 1:1 conversation is used, proteus 1:1 conversation will be deleted locally.
- const {conversationId} = connectionEntity;
+ const {conversationId: proteusConversationId, userId: otherUserId} = connectionEntity;
+ const localProteusConversation = this.conversationState.findConversation(proteusConversationId);
- const localConversation = this.conversationState.findConversation(conversationId);
+ if (connectionEntity.isOutgoingRequest()) {
+ //return type of 3 (connect) conversation
+ //it will be displayed as a connection request
+ return localProteusConversation || this.fetchConversationById(proteusConversationId);
+ }
const isConnectionAccepted = connectionEntity.isConnected();
- const shouldFetchConversation = isConnectionAccepted || connectionEntity.isOutgoingRequest();
+ //check what protocol should be used for 1:1 conversation
+ const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(otherUserId);
- const conversation =
- localConversation || (shouldFetchConversation ? await this.fetchConversationById(conversationId) : undefined);
+ //if it's not connection request and conversation is not accepted,
+ if (!isConnectionAccepted) {
+ const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
+
+ //if we already know mls 1:1 conversation, we use it, even if proteus protocol was now choosen as common
+ //we do not support switching back to proteus after mls conversation was established
+ //only proteus -> mls migration is supported, never the other way around
+ if (localMLSConversation) {
+ //make sure proteus conversation is gone, we don't want to see it anymore
+ if (localProteusConversation && isProteusConversation(localProteusConversation)) {
+ await this.replaceProteus1to1WithMLS(localProteusConversation, localMLSConversation);
+ }
+ return localMLSConversation;
+ }
- if (!conversation) {
- return undefined;
+ return protocol === ConversationProtocol.PROTEUS ? localProteusConversation : undefined;
}
- if (isConnectionAccepted) {
- conversation.type(CONVERSATION_TYPE.ONE_TO_ONE);
- return this.init1to1Conversation(conversation);
+ //if it's accepted, initialise conversation so it's ready to be used
+ if (protocol === ConversationProtocol.MLS) {
+ const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
+ if (localProteusConversation && isProteusConversation(localProteusConversation)) {
+ await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
+ }
}
- return conversation;
+ if (protocol === ConversationProtocol.PROTEUS) {
+ return this.initProteus1to1Conversation(proteusConversationId, isSupportedByTheOtherUser);
+ }
+
+ return undefined;
};
/**
@@ -1712,14 +1719,17 @@ export class ConversationRepository {
return undefined;
}
- connectionEntity.conversationId = conversation.qualifiedId;
conversation.connection(connectionEntity);
- await this.updateParticipatingUserEntities(conversation);
+ if (connectionEntity.isConnected()) {
+ conversation.type(CONVERSATION_TYPE.ONE_TO_ONE);
+ }
+
+ const updatedConversation = await this.updateParticipatingUserEntities(conversation);
this.conversationState.conversations.notifySubscribers();
- return conversation;
+ return updatedConversation;
} catch (error) {
const isConversationNotFound =
error instanceof ConversationError && error.type === ConversationError.TYPE.CONVERSATION_NOT_FOUND;
diff --git a/src/script/main/app.ts b/src/script/main/app.ts
index 49e62f6fcf8..2fd5a1ab07d 100644
--- a/src/script/main/app.ts
+++ b/src/script/main/app.ts
@@ -433,11 +433,6 @@ export class App {
});
const notificationsCount = eventRepository.notificationsTotal;
- const conversations1to1 = conversations.filter(conversation => conversation.is1to1());
- await Promise.allSettled(
- conversations1to1.map(conversation => conversationRepository.init1to1Conversation(conversation)),
- );
-
if (connections.length) {
await Promise.allSettled(conversationRepository.mapConnections(connections));
}
From e3ab1abf1230c3abc062cb34e94d1ab9401f346c Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 7 Aug 2023 15:46:05 +0900
Subject: [PATCH 14/70] runfix: add missing return statement
---
src/script/conversation/ConversationRepository.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 7396d98d3e6..5219d1ea370 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1635,7 +1635,11 @@ export class ConversationRepository {
//we never go back to proteus conversation, even if one of the users do not support mls anymore
//(e.g. due to the change of supported protocols in team configuration)
if (shouldUseMLSProtocol || isMLSConversation(conversation)) {
- return this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
+ const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
+ if (isProteusConversation(conversation)) {
+ await this.replaceProteus1to1WithMLS(conversation, mlsConversation);
+ }
+ return mlsConversation;
}
if (protocol === ConversationProtocol.PROTEUS && isProteusConversation(conversation)) {
@@ -1690,6 +1694,7 @@ export class ConversationRepository {
if (localProteusConversation && isProteusConversation(localProteusConversation)) {
await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
}
+ return mlsConversation;
}
if (protocol === ConversationProtocol.PROTEUS) {
From 2a0fa9226c37dffb0db0b275754084af72278744 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 14 Aug 2023 12:36:02 +0900
Subject: [PATCH 15/70] feat: get 1to1 team conversation
---
src/script/conversation/ConversationRepository.ts | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 5219d1ea370..ef9176fe992 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -70,7 +70,6 @@ import {
} from 'Util/StringUtil';
import {TIME_IN_MILLIS} from 'Util/TimeUtil';
import {isBackendError} from 'Util/TypePredicateUtil';
-import {supportsMLS1To1Conversations} from 'Util/util';
import {createUuid} from 'Util/uuid';
import {ACCESS_STATE} from './AccessState';
@@ -1198,7 +1197,6 @@ export class ConversationRepository {
const conversationId = userEntity.connection().conversationId;
try {
- //TODO: check if mls conversation will be returned when fetched with this endpoint
const conversationEntity = await this.getConversationById(conversationId);
conversationEntity.connection(userEntity.connection());
await this.updateParticipatingUserEntities(conversationEntity);
@@ -1391,7 +1389,7 @@ export class ConversationRepository {
* @param otherUserId - id of the other user
* @returns MLS conversation entity
*/
- private readonly findMLS1to1Conversation = async (otherUserId: QualifiedId): Promise => {
+ private readonly getMLS1to1Conversation = async (otherUserId: QualifiedId): Promise => {
const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
if (localMLSConversation) {
@@ -1536,7 +1534,7 @@ export class ConversationRepository {
isMLSSupportedByTheOtherUser: boolean,
): Promise => {
this.logger.info(`Initialising MLS 1:1 conversation with user ${otherUserId.id}...`);
- const mlsConversation = await this.findMLS1to1Conversation(otherUserId);
+ const mlsConversation = await this.getMLS1to1Conversation(otherUserId);
//if mls is not supported by the other user we do not establish the group yet
//we just mark the mls conversation as readonly and return it
@@ -1629,12 +1627,10 @@ export class ConversationRepository {
`Protocol for 1:1 conversation ${conversation.id} with user ${otherUserId.id} is ${protocol}, isSupportedByTheOtherUser: ${isSupportedByTheOtherUser}`,
);
- const shouldUseMLSProtocol = protocol === ConversationProtocol.MLS && supportsMLS1To1Conversations();
-
//if both users support mls or mls conversation is already established, we use it
//we never go back to proteus conversation, even if one of the users do not support mls anymore
//(e.g. due to the change of supported protocols in team configuration)
- if (shouldUseMLSProtocol || isMLSConversation(conversation)) {
+ if (protocol === ConversationProtocol.MLS || isMLSConversation(conversation)) {
const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
if (isProteusConversation(conversation)) {
await this.replaceProteus1to1WithMLS(conversation, mlsConversation);
From c37d96e5e7bbbbbb7086087c9ccb7a016e80f16d Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Wed, 16 Aug 2023 14:59:12 +0900
Subject: [PATCH 16/70] feat: assign user connection to conversation
---
.../ConnectRequests/ConnectionRequests.tsx | 3 +-
.../conversation/ConversationRepository.ts | 66 +++++++------------
2 files changed, 24 insertions(+), 45 deletions(-)
diff --git a/src/script/components/ConnectRequests/ConnectionRequests.tsx b/src/script/components/ConnectRequests/ConnectionRequests.tsx
index f011ac7184a..885f5a47afd 100644
--- a/src/script/components/ConnectRequests/ConnectionRequests.tsx
+++ b/src/script/components/ConnectRequests/ConnectionRequests.tsx
@@ -93,13 +93,14 @@ export const ConnectRequests: FC = ({
const onAcceptClick = async (userEntity: User) => {
await actionsViewModel.acceptConnectionRequest(userEntity);
+
const conversationEntity = await actionsViewModel.getOrCreate1to1Conversation(userEntity);
if (connectionRequests.length === 1) {
/**
* In the connect request view modal, we show an overview of all incoming connection requests. When there are multiple open connection requests, we want that the user sees them all and can accept them one-by-one. When the last open connection request gets accepted, we want the user to switch to this conversation.
*/
- actionsViewModel.open1to1Conversation(conversationEntity);
+ return actionsViewModel.open1to1Conversation(conversationEntity);
}
};
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index ef9176fe992..765189fafa8 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1418,32 +1418,6 @@ export class ConversationRepository {
return conversation;
};
- private readonly isMLSConversationEstablished = async (groupId: string) => {
- const mlsService = this.core.service?.mls;
-
- if (!mlsService) {
- throw new Error('MLS service is not available!');
- }
-
- const mlsConversationState = useMLSConversationState.getState();
-
- const isMLSConversationMarkedAsEstablished = mlsConversationState.isEstablished(groupId);
-
- if (isMLSConversationMarkedAsEstablished) {
- return true;
- }
-
- const isMLSConversationEstablished = await mlsService.conversationExists(groupId);
-
- if (isMLSConversationEstablished) {
- //make sure MLS group is marked as established
- mlsConversationState.markAsEstablished(groupId);
- return true;
- }
-
- return false;
- };
-
/**
* Will migrate proteus 1:1 conversation to mls 1:1 conversation.
* All the events will be moved to the new conversation and proteus conversation will be deleted locally.
@@ -1467,9 +1441,7 @@ export class ConversationRepository {
}
//TODO:
- // if (proteusConversation.isTeam1to1()) {
// this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
- // }
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
@@ -1492,34 +1464,35 @@ export class ConversationRepository {
* @param otherUserId - id of the other user
* @param proteusConversation - (optional) proteus 1:1 conversation
*/
- private readonly establishMLS1to1Conversation = async (
+ public readonly establishMLS1to1Conversation = async (
mlsConversation: MLSConversation,
otherUserId: QualifiedId,
): Promise => {
- const mlsService = this.core.service?.mls;
+ const conversationService = this.core.service?.conversation;
- if (!mlsService) {
- throw new Error('MLS service is not available!');
+ if (!conversationService) {
+ throw new Error('Conversation service is not available!');
}
- const {groupId} = mlsConversation;
+ const {groupId, qualifiedId} = mlsConversation;
- //if epoch is higher that 0 it means that the group is already established but we don't have it in our local state
- //we have to join with external commit
+ //if epoch is higher that 0 it means that the group is already established, we have to join with external commit
if (mlsConversation.epoch > 0) {
- await joinNewConversations([mlsConversation], this.core);
+ await conversationService.joinByExternalCommit(qualifiedId);
return mlsConversation;
}
- //if it's not established, establish it and add the other user to the group
const selfUserId = this.userState.self().qualifiedId;
- await mlsService.registerConversation(groupId, [otherUserId, selfUserId], {
- user: selfUserId,
- client: this.core.clientId,
- });
- useMLSConversationState.getState().markAsEstablished(groupId);
- return mlsConversation;
+ //if it's not established, establish it and add the other user to the group
+ await conversationService.establishMLS1to1Conversation(
+ groupId,
+ {client: this.core.clientId, user: selfUserId},
+ otherUserId,
+ );
+
+ //refetch the conversation to get the letest epoch and updated participants list
+ return this.fetchMLS1to1Conversation(otherUserId);
};
/**
@@ -1536,6 +1509,10 @@ export class ConversationRepository {
this.logger.info(`Initialising MLS 1:1 conversation with user ${otherUserId.id}...`);
const mlsConversation = await this.getMLS1to1Conversation(otherUserId);
+ const otherUser = await this.userRepository.getUserById(otherUserId);
+ mlsConversation.connection(otherUser.connection());
+ await this.updateParticipatingUserEntities(mlsConversation);
+
//if mls is not supported by the other user we do not establish the group yet
//we just mark the mls conversation as readonly and return it
if (!isMLSSupportedByTheOtherUser) {
@@ -1546,13 +1523,14 @@ export class ConversationRepository {
return mlsConversation;
}
- const isAlreadyEstablished = await this.isMLSConversationEstablished(mlsConversation.groupId);
+ const isAlreadyEstablished = await this.conversationService.isMLSConversationEstablished(mlsConversation.groupId);
const establishedMLSConversation = isAlreadyEstablished
? mlsConversation
: await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
this.logger.info(`MLS 1:1 conversation with user ${otherUserId.id} is established.`);
+
return establishedMLSConversation;
};
From 6943db7a6e07261755bac3658b1e059ad17e83a3 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Wed, 16 Aug 2023 16:32:34 +0900
Subject: [PATCH 17/70] chore: add todo
---
src/script/conversation/ConversationRepository.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 765189fafa8..7d13ea45cd5 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1441,7 +1441,8 @@ export class ConversationRepository {
}
//TODO:
- // this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
+ //check if this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
+ //is needed
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
@@ -1454,6 +1455,8 @@ export class ConversationRepository {
this.logger.info(`Deleting proteus 1:1 conversation ${proteusConversation.id}`);
await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
+
+ //TODO: maintain the list of retired proteus 1:1 conversations so they are not requested from backend anymore
};
/**
From 24166c56e54729244981419be02b98d62e96bfab Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 17 Aug 2023 16:55:24 +0900
Subject: [PATCH 18/70] feat: hide proteus 1:1 after receiving welcome to mls
1:1
---
.../conversation/ConversationRepository.ts | 76 ++++++++++++++-----
src/script/conversation/ConversationState.ts | 57 ++++++++++----
2 files changed, 101 insertions(+), 32 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 7d13ea45cd5..d103f4f74b4 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -41,6 +41,7 @@ import {
CONVERSATION_EVENT,
FederationEvent,
FEDERATION_EVENT,
+ ConversationMLSWelcomeEvent,
} from '@wireapp/api-client/lib/event';
import {BackendErrorLabel} from '@wireapp/api-client/lib/http/';
import type {BackendError} from '@wireapp/api-client/lib/http/';
@@ -1446,17 +1447,16 @@ export class ConversationRepository {
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
+ this.logger.info(`Deleting proteus 1:1 conversation ${proteusConversation.id}`);
+ await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
+ //TODO: maintain the list of retired proteus 1:1 conversations so they are not requested from backend anymore
+
if (isActiveConversation) {
this.logger.info(
`Proteus 1:1 conversation ${proteusConversation.id} is active conversation, showing mls 1:1 conversation ${mlsConversation.id}`,
);
amplify.publish(WebAppEvents.CONVERSATION.SHOW, mlsConversation, {});
}
-
- this.logger.info(`Deleting proteus 1:1 conversation ${proteusConversation.id}`);
- await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
-
- //TODO: maintain the list of retired proteus 1:1 conversations so they are not requested from backend anymore
};
/**
@@ -1608,10 +1608,18 @@ export class ConversationRepository {
`Protocol for 1:1 conversation ${conversation.id} with user ${otherUserId.id} is ${protocol}, isSupportedByTheOtherUser: ${isSupportedByTheOtherUser}`,
);
- //if both users support mls or mls conversation is already established, we use it
+ //when called with mls conversation, we just initialise it
+ if (isMLSConversation(conversation)) {
+ return this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
+ }
+
+ //if there's local mls conversation, return it
+ const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
+
+ //if both users support mls or mls conversation is already known, we use it
//we never go back to proteus conversation, even if one of the users do not support mls anymore
//(e.g. due to the change of supported protocols in team configuration)
- if (protocol === ConversationProtocol.MLS || isMLSConversation(conversation)) {
+ if (protocol === ConversationProtocol.MLS || localMLSConversation) {
const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
if (isProteusConversation(conversation)) {
await this.replaceProteus1to1WithMLS(conversation, mlsConversation);
@@ -1647,26 +1655,27 @@ export class ConversationRepository {
//check what protocol should be used for 1:1 conversation
const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(otherUserId);
- //if it's not connection request and conversation is not accepted,
- if (!isConnectionAccepted) {
- const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
+ const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
+ // It's not connection request and conversation is not accepted,
+ if (!isConnectionAccepted) {
//if we already know mls 1:1 conversation, we use it, even if proteus protocol was now choosen as common
//we do not support switching back to proteus after mls conversation was established
//only proteus -> mls migration is supported, never the other way around
- if (localMLSConversation) {
- //make sure proteus conversation is gone, we don't want to see it anymore
- if (localProteusConversation && isProteusConversation(localProteusConversation)) {
- await this.replaceProteus1to1WithMLS(localProteusConversation, localMLSConversation);
- }
- return localMLSConversation;
+ if (!localMLSConversation) {
+ return protocol === ConversationProtocol.PROTEUS ? localProteusConversation : undefined;
+ }
+
+ //make sure proteus conversation is gone, we don't want to see it anymore
+ if (localProteusConversation && isProteusConversation(localProteusConversation)) {
+ await this.replaceProteus1to1WithMLS(localProteusConversation, localMLSConversation);
}
- return protocol === ConversationProtocol.PROTEUS ? localProteusConversation : undefined;
+ return localMLSConversation;
}
//if it's accepted, initialise conversation so it's ready to be used
- if (protocol === ConversationProtocol.MLS) {
+ if (protocol === ConversationProtocol.MLS || localMLSConversation) {
const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
if (localProteusConversation && isProteusConversation(localProteusConversation)) {
await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
@@ -2753,6 +2762,9 @@ export class ConversationRepository {
case CONVERSATION_EVENT.RENAME:
return this.onRename(conversationEntity, eventJson, eventSource === EventRepository.SOURCE.WEB_SOCKET);
+ case CONVERSATION_EVENT.MLS_WELCOME_MESSAGE:
+ return this.onMLSWelcomeMessage(conversationEntity, eventJson);
+
case ClientEvent.CONVERSATION.ASSET_ADD:
return this.onAssetAdd(conversationEntity, eventJson);
@@ -3437,6 +3449,34 @@ export class ConversationRepository {
return {conversationEntity, messageEntity};
}
+ /**
+ * User has received a welcome message in a conversation.
+ *
+ * @param conversationEntity Conversation entity user has received a welcome message in
+ * @param eventJson JSON data of 'conversation.mls-welcome' event
+ * @returns Resolves when the event was handled
+ */
+ private async onMLSWelcomeMessage(conversationEntity: Conversation, eventJson: ConversationMLSWelcomeEvent) {
+ // If we receive a welcome message in mls 1:1 conversation, we need to make sure proteus 1:1 is hidden (if it exists)
+
+ if (conversationEntity.type() !== CONVERSATION_TYPE.ONE_TO_ONE || !isMLSConversation(conversationEntity)) {
+ return;
+ }
+
+ const otherUserId = eventJson.qualified_from;
+
+ if (!otherUserId) {
+ return;
+ }
+
+ const proteus1to1Conversation = this.conversationState.findProteus1to1Conversation(otherUserId);
+
+ const mlsConversation = await this.initMLS1to1Conversation(otherUserId, true);
+ if (proteus1to1Conversation) {
+ await this.replaceProteus1to1WithMLS(proteus1to1Conversation, mlsConversation);
+ }
+ }
+
/**
* A user started or stopped typing in a conversation.
*
diff --git a/src/script/conversation/ConversationState.ts b/src/script/conversation/ConversationState.ts
index 37569af88cc..ed583dd1984 100644
--- a/src/script/conversation/ConversationState.ts
+++ b/src/script/conversation/ConversationState.ts
@@ -26,7 +26,13 @@ import {container, singleton} from 'tsyringe';
import {matchQualifiedIds} from 'Util/QualifiedId';
import {sortGroupsByLastEvent} from 'Util/util';
-import {MLSConversation, isMLSConversation, isSelfConversation} from './ConversationSelectors';
+import {
+ MLSConversation,
+ ProteusConversation,
+ isMLSConversation,
+ isProteusConversation,
+ isSelfConversation,
+} from './ConversationSelectors';
import {Conversation} from '../entity/Conversation';
import {User} from '../entity/User';
@@ -197,19 +203,42 @@ export class ConversationState {
* Find a local MLS 1:1 conversation by user Id.
* @returns Conversation is locally available
*/
- findMLS1to1Conversation(userId: QualifiedId): MLSConversation | undefined {
- return this.conversations().find((conversation): conversation is MLSConversation => {
- const conversationMembersIds = conversation.participating_user_ids();
- const otherUserId = conversationMembersIds[0];
-
- return (
- isMLSConversation(conversation) &&
- conversation.type() === CONVERSATION_TYPE.ONE_TO_ONE &&
- conversationMembersIds.length === 1 &&
- otherUserId &&
- otherUserId.id === userId.id
- );
- });
+ findMLS1to1Conversation(userId: QualifiedId): MLSConversation | null {
+ return (
+ this.conversations().find((conversation): conversation is MLSConversation => {
+ const conversationMembersIds = conversation.participating_user_ids();
+ const otherUserId = conversationMembersIds[0];
+
+ return (
+ conversation.type() === CONVERSATION_TYPE.ONE_TO_ONE &&
+ isMLSConversation(conversation) &&
+ conversationMembersIds.length === 1 &&
+ otherUserId &&
+ otherUserId.id === userId.id
+ );
+ }) || null
+ );
+ }
+
+ /**
+ * Find a local Proteus 1:1 conversation by user Id.
+ * @returns Proteus 1:1 conversation if locally available
+ */
+ findProteus1to1Conversation(userId: QualifiedId): ProteusConversation | null {
+ return (
+ this.conversations().find((conversation): conversation is ProteusConversation => {
+ const conversationMembersIds = conversation.participating_user_ids();
+ const otherUserId = conversationMembersIds[0];
+
+ return (
+ conversation.type() === CONVERSATION_TYPE.ONE_TO_ONE &&
+ isProteusConversation(conversation) &&
+ conversationMembersIds.length === 1 &&
+ otherUserId &&
+ otherUserId.id === userId.id
+ );
+ }) || null
+ );
}
isSelfConversation(conversationId: QualifiedId): boolean {
From fe6f1982d0c462e6417d64d720daeff8ec6576c8 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 17 Aug 2023 17:25:32 +0900
Subject: [PATCH 19/70] chore: remove mls 1:1 feature flags
---
.env.localhost | 1 -
server/config/client.config.ts | 1 -
server/config/env.ts | 3 ---
src/script/util/util.ts | 3 ---
4 files changed, 8 deletions(-)
diff --git a/.env.localhost b/.env.localhost
index 9ea0c43e8a2..dee76fb703c 100644
--- a/.env.localhost
+++ b/.env.localhost
@@ -21,7 +21,6 @@ ENABLE_DEV_BACKEND_API="true"
#FEATURE_APPLOCK_UNFOCUS_TIMEOUT="30"
#FEATURE_APPLOCK_SCHEDULED_TIMEOUT="30"
#FEATURE_ENABLE_MLS="true"
-#FEATURE_ENABLE_MLS_1_TO_1_CONVERSATIONS="true"
#FEATURE_ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES="true"
#FEATURE_USE_CORE_CRYPTO="true"
diff --git a/server/config/client.config.ts b/server/config/client.config.ts
index 8e2b0fbecca..71756cee5e2 100644
--- a/server/config/client.config.ts
+++ b/server/config/client.config.ts
@@ -55,7 +55,6 @@ export function generateConfig(params: ConfigGeneratorParams, env: Env) {
ENABLE_EXTRA_CLIENT_ENTROPY: env.FEATURE_ENABLE_EXTRA_CLIENT_ENTROPY == 'true',
ENABLE_MEDIA_EMBEDS: env.FEATURE_ENABLE_MEDIA_EMBEDS != 'false',
ENABLE_MLS: env.FEATURE_ENABLE_MLS == 'true',
- ENABLE_MLS_1_TO_1_CONVERSATIONS: env.FEATURE_ENABLE_MLS_1_TO_1_CONVERSATIONS == 'true',
ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES: env.FEATURE_ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES == 'true',
ENABLE_PHONE_LOGIN: env.FEATURE_ENABLE_PHONE_LOGIN != 'false',
ENABLE_PROTEUS_CORE_CRYPTO: env.FEATURE_ENABLE_PROTEUS_CORE_CRYPTO == 'true',
diff --git a/server/config/env.ts b/server/config/env.ts
index 83461664188..765f646cd11 100644
--- a/server/config/env.ts
+++ b/server/config/env.ts
@@ -80,9 +80,6 @@ export type Env = {
/** will enable the MLS protocol */
FEATURE_ENABLE_MLS?: string;
- /** will enable the MLS protocol for 1:1 conversations */
- FEATURE_ENABLE_MLS_1_TO_1_CONVERSATIONS?: string;
-
/** will enable the user to periodically update the list of supported protocols */
FEATURE_ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES?: string;
diff --git a/src/script/util/util.ts b/src/script/util/util.ts
index 1aa250b520e..871c2af92a5 100644
--- a/src/script/util/util.ts
+++ b/src/script/util/util.ts
@@ -405,9 +405,6 @@ const supportsSecretStorage = () => !Runtime.isDesktopApp() || !!window.systemCr
// disables mls for old 'broken' desktop clients, see https://github.com/wireapp/wire-desktop/pull/6094
export const supportsMLS = () => Config.getConfig().FEATURE.ENABLE_MLS && supportsSecretStorage();
-export const supportsMLS1To1Conversations = () =>
- supportsMLS() && Config.getConfig().FEATURE.ENABLE_MLS_1_TO_1_CONVERSATIONS;
-
export const supportsSelfSupportedProtocolsUpdates = () =>
Config.getConfig().FEATURE.ENABLE_SELF_SUPPORTED_PROTOCOLS_UPDATES;
From 4e67c806f6e81fc058a8f9ab5fe5f45e47538bf3 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 17 Aug 2023 18:12:54 +0900
Subject: [PATCH 20/70] refactor: find 1:1 by uid and protocol
---
.../conversation/ConversationRepository.ts | 11 ++--
.../conversation/ConversationSelectors.ts | 5 ++
src/script/conversation/ConversationState.ts | 63 ++++++++-----------
3 files changed, 39 insertions(+), 40 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index d103f4f74b4..dfa2b611ae7 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1391,7 +1391,7 @@ export class ConversationRepository {
* @returns MLS conversation entity
*/
private readonly getMLS1to1Conversation = async (otherUserId: QualifiedId): Promise => {
- const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
+ const localMLSConversation = this.conversationState.find1to1Conversation(otherUserId, ConversationProtocol.MLS);
if (localMLSConversation) {
return localMLSConversation;
@@ -1614,7 +1614,7 @@ export class ConversationRepository {
}
//if there's local mls conversation, return it
- const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
+ const localMLSConversation = this.conversationState.find1to1Conversation(otherUserId, ConversationProtocol.MLS);
//if both users support mls or mls conversation is already known, we use it
//we never go back to proteus conversation, even if one of the users do not support mls anymore
@@ -1655,7 +1655,7 @@ export class ConversationRepository {
//check what protocol should be used for 1:1 conversation
const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(otherUserId);
- const localMLSConversation = this.conversationState.findMLS1to1Conversation(otherUserId);
+ const localMLSConversation = this.conversationState.find1to1Conversation(otherUserId, ConversationProtocol.MLS);
// It's not connection request and conversation is not accepted,
if (!isConnectionAccepted) {
@@ -3469,7 +3469,10 @@ export class ConversationRepository {
return;
}
- const proteus1to1Conversation = this.conversationState.findProteus1to1Conversation(otherUserId);
+ const proteus1to1Conversation = this.conversationState.find1to1Conversation(
+ otherUserId,
+ ConversationProtocol.PROTEUS,
+ );
const mlsConversation = await this.initMLS1to1Conversation(otherUserId, true);
if (proteus1to1Conversation) {
diff --git a/src/script/conversation/ConversationSelectors.ts b/src/script/conversation/ConversationSelectors.ts
index bf1295d4175..5bcaf32b297 100644
--- a/src/script/conversation/ConversationSelectors.ts
+++ b/src/script/conversation/ConversationSelectors.ts
@@ -26,6 +26,11 @@ export type MixedConversation = Conversation & {groupId: string; protocol: Conve
export type MLSConversation = Conversation & {groupId: string; protocol: ConversationProtocol.MLS};
export type MLSCapableConversation = MixedConversation | MLSConversation;
+export interface ProtocolToConversationType {
+ [ConversationProtocol.PROTEUS]: ProteusConversation;
+ [ConversationProtocol.MLS]: MLSConversation;
+}
+
export function isProteusConversation(conversation: Conversation): conversation is ProteusConversation {
return !conversation.groupId && conversation.protocol === ConversationProtocol.PROTEUS;
}
diff --git a/src/script/conversation/ConversationState.ts b/src/script/conversation/ConversationState.ts
index ed583dd1984..309fac73100 100644
--- a/src/script/conversation/ConversationState.ts
+++ b/src/script/conversation/ConversationState.ts
@@ -18,7 +18,7 @@
*/
import {ConnectionStatus} from '@wireapp/api-client/lib/connection/';
-import {CONVERSATION_TYPE} from '@wireapp/api-client/lib/conversation';
+import {ConversationProtocol} from '@wireapp/api-client/lib/conversation';
import {QualifiedId} from '@wireapp/api-client/lib/user';
import ko from 'knockout';
import {container, singleton} from 'tsyringe';
@@ -27,8 +27,7 @@ import {matchQualifiedIds} from 'Util/QualifiedId';
import {sortGroupsByLastEvent} from 'Util/util';
import {
- MLSConversation,
- ProteusConversation,
+ ProtocolToConversationType,
isMLSConversation,
isProteusConversation,
isSelfConversation,
@@ -200,45 +199,37 @@ export class ConversationState {
}
/**
- * Find a local MLS 1:1 conversation by user Id.
- * @returns Conversation is locally available
+ * Find a local 1:1 conversation by user Id and procotol (proteus or mls).
+ * @returns Conversation if locally available, otherwise null
*/
- findMLS1to1Conversation(userId: QualifiedId): MLSConversation | null {
- return (
- this.conversations().find((conversation): conversation is MLSConversation => {
- const conversationMembersIds = conversation.participating_user_ids();
- const otherUserId = conversationMembersIds[0];
+ find1to1Conversation(
+ userId: QualifiedId,
+ protocol: Protocol,
+ ): ProtocolToConversationType[Protocol] | null {
+ const foundConversation = this.conversations().find(
+ (conversation): conversation is ProtocolToConversationType[Protocol] => {
+ const doesProtocolMatch =
+ protocol === ConversationProtocol.PROTEUS
+ ? isProteusConversation(conversation)
+ : isMLSConversation(conversation);
+
+ if (!doesProtocolMatch) {
+ return false;
+ }
- return (
- conversation.type() === CONVERSATION_TYPE.ONE_TO_ONE &&
- isMLSConversation(conversation) &&
- conversationMembersIds.length === 1 &&
- otherUserId &&
- otherUserId.id === userId.id
- );
- }) || null
- );
- }
+ if (!conversation.is1to1()) {
+ return false;
+ }
- /**
- * Find a local Proteus 1:1 conversation by user Id.
- * @returns Proteus 1:1 conversation if locally available
- */
- findProteus1to1Conversation(userId: QualifiedId): ProteusConversation | null {
- return (
- this.conversations().find((conversation): conversation is ProteusConversation => {
const conversationMembersIds = conversation.participating_user_ids();
- const otherUserId = conversationMembersIds[0];
+ const otherUserQualifiedId = conversationMembersIds.length === 1 ? conversationMembersIds[0] : null;
+ const doesUserIdMatch = !!otherUserQualifiedId && otherUserQualifiedId.id === userId.id;
- return (
- conversation.type() === CONVERSATION_TYPE.ONE_TO_ONE &&
- isProteusConversation(conversation) &&
- conversationMembersIds.length === 1 &&
- otherUserId &&
- otherUserId.id === userId.id
- );
- }) || null
+ return doesUserIdMatch;
+ },
);
+
+ return foundConversation || null;
}
isSelfConversation(conversationId: QualifiedId): boolean {
From a352ef137203e43efef13750c50c4e8abfb20367 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 17 Aug 2023 18:43:48 +0900
Subject: [PATCH 21/70] runfix: insert group creation message to 1:1 mls after
replcing
---
src/script/conversation/ConversationRepository.ts | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index dfa2b611ae7..b022fc4dfa2 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1441,9 +1441,9 @@ export class ConversationRepository {
await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
}
- //TODO:
- //check if this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
- //is needed
+ if (!mlsConversation.hasCreationMessage) {
+ this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
+ }
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
@@ -1677,9 +1677,11 @@ export class ConversationRepository {
//if it's accepted, initialise conversation so it's ready to be used
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
+
if (localProteusConversation && isProteusConversation(localProteusConversation)) {
await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
}
+
return mlsConversation;
}
From c4acf48dd1efeb9b82e1eea218dc1dc87906323c Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 28 Aug 2023 09:47:50 +0200
Subject: [PATCH 22/70] test: fix conversation repo test
---
src/__mocks__/@wireapp/core.ts | 1 -
.../conversation/ConversationRepository.test.ts | 11 ++++++++---
src/script/conversation/ConversationRepository.ts | 2 +-
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/__mocks__/@wireapp/core.ts b/src/__mocks__/@wireapp/core.ts
index a064774230f..533f85edb78 100644
--- a/src/__mocks__/@wireapp/core.ts
+++ b/src/__mocks__/@wireapp/core.ts
@@ -38,7 +38,6 @@ export class Account extends EventEmitter {
conversationExists: jest.fn(),
exportSecretKey: jest.fn(),
leaveConferenceSubconversation: jest.fn(),
- conversationExists: jest.fn(),
on: this.on,
emit: this.emit,
off: this.off,
diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts
index ab4f88efac2..db2a88a13ef 100644
--- a/src/script/conversation/ConversationRepository.test.ts
+++ b/src/script/conversation/ConversationRepository.test.ts
@@ -255,13 +255,18 @@ describe('ConversationRepository', () => {
const teamId = team1to1Conversation.team;
const teamMemberId = team1to1Conversation.members.others[0].id;
const userEntity = new User(teamMemberId, 'test-domain');
+ userEntity.inTeam(true);
+ userEntity.isTeamMember(true);
+ userEntity.teamId = teamId;
+ userEntity.supportedProtocols([ConversationProtocol.PROTEUS]);
+
+ testFactory.user_repository['userState'].users.push(userEntity);
const selfUser = generateUser();
selfUser.teamId = teamId;
+ selfUser.supportedProtocols([ConversationProtocol.PROTEUS]);
+
spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser);
- userEntity.inTeam(true);
- userEntity.isTeamMember(true);
- userEntity.teamId = teamId;
return testFactory.conversation_repository.get1To1Conversation(userEntity).then(conversationEntity => {
expect(conversationEntity).toBe(newConversationEntity);
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index b022fc4dfa2..654741dc881 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1619,7 +1619,7 @@ export class ConversationRepository {
//if both users support mls or mls conversation is already known, we use it
//we never go back to proteus conversation, even if one of the users do not support mls anymore
//(e.g. due to the change of supported protocols in team configuration)
- if (protocol === ConversationProtocol.MLS || localMLSConversation) {
+ if (localMLSConversation || protocol === ConversationProtocol.MLS) {
const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
if (isProteusConversation(conversation)) {
await this.replaceProteus1to1WithMLS(conversation, mlsConversation);
From 0eb23390ff1797afdb19321b7880df8bf2334573 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Tue, 29 Aug 2023 11:47:11 +0200
Subject: [PATCH 23/70] test: update loadUsers case
---
src/script/user/UserRepository.test.ts | 16 +++-------------
1 file changed, 3 insertions(+), 13 deletions(-)
diff --git a/src/script/user/UserRepository.test.ts b/src/script/user/UserRepository.test.ts
index 0d810fd70eb..f7269646f37 100644
--- a/src/script/user/UserRepository.test.ts
+++ b/src/script/user/UserRepository.test.ts
@@ -214,26 +214,16 @@ describe('UserRepository', () => {
userState.users([selfUser]);
});
- it('loads all users from backend if no users are stored locally', async () => {
+ it('loads all users from backend even when they are already known locally', async () => {
const newUsers = [generateAPIUser(), generateAPIUser()];
const users = [...localUsers, ...newUsers];
const userIds = users.map(user => user.qualified_id!);
- const fetchUserSpy = jest.spyOn(userRepository['userService'], 'getUsers').mockResolvedValue({found: newUsers});
+ const fetchUserSpy = jest.spyOn(userRepository['userService'], 'getUsers').mockResolvedValue({found: users});
await userRepository.loadUsers(new User('self'), [], [], userIds);
expect(userState.users()).toHaveLength(users.length + 1);
- expect(fetchUserSpy).toHaveBeenCalledWith(newUsers.map(user => user.qualified_id!));
- });
-
- it('does not load users from backend if they are already in database', async () => {
- const userIds = localUsers.map(user => user.qualified_id!);
- const fetchUserSpy = jest.spyOn(userRepository['userService'], 'getUsers').mockResolvedValue({found: []});
-
- await userRepository.loadUsers(new User('self'), [], [], userIds);
-
- expect(userState.users()).toHaveLength(localUsers.length + 1);
- expect(fetchUserSpy).not.toHaveBeenCalled();
+ expect(fetchUserSpy).toHaveBeenCalledWith(users.map(user => user.qualified_id!));
});
it('loads users that are partially stored in the DB and maps availability', async () => {
From bab52cae5a9ef10495c25a0e96273fb0f4566264 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Wed, 30 Aug 2023 16:07:04 +0200
Subject: [PATCH 24/70] chore: add todo comment
---
src/script/conversation/ConversationRepository.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 654741dc881..3f34760a72f 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1513,6 +1513,8 @@ export class ConversationRepository {
const mlsConversation = await this.getMLS1to1Conversation(otherUserId);
const otherUser = await this.userRepository.getUserById(otherUserId);
+
+ //TODO: for team 1:1 conversation we don't need to have connection assigned
mlsConversation.connection(otherUser.connection());
await this.updateParticipatingUserEntities(mlsConversation);
From f73ac13723c37fad629eb99fbf643c1e1593368c Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 31 Aug 2023 12:52:28 +0200
Subject: [PATCH 25/70] test: fix test
---
src/script/conversation/ConversationRepository.test.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts
index db2a88a13ef..71122ebb0ec 100644
--- a/src/script/conversation/ConversationRepository.test.ts
+++ b/src/script/conversation/ConversationRepository.test.ts
@@ -413,6 +413,7 @@ describe('ConversationRepository', () => {
spyOn(testFactory.conversation_service, 'getConversationById').and.returnValue(
Promise.resolve(conversation_payload),
);
+ spyOn(testFactory.user_repository, 'getUsersById').and.returnValue(Promise.resolve([]));
});
it('should map a connection to an existing conversation', () => {
From 61cf75260895372ac4883675432b9117ed50034c Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Fri, 1 Sep 2023 11:38:42 +0200
Subject: [PATCH 26/70] feat: get1to1Conversation
---
.../conversation/ConversationRepository.ts | 36 ++++++++++++++++++-
1 file changed, 35 insertions(+), 1 deletion(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 3f34760a72f..37173e9e2dd 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1189,6 +1189,40 @@ export class ConversationRepository {
* @returns Resolves with the conversation with requested user
*/
async get1To1Conversation(userEntity: User): Promise {
+ const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(userEntity);
+
+ const localMLSConversation = this.conversationState.find1to1Conversation(
+ userEntity.qualifiedId,
+ ConversationProtocol.MLS,
+ );
+
+ const localProteusConversation = this.conversationState.find1to1Conversation(
+ userEntity.qualifiedId,
+ ConversationProtocol.PROTEUS,
+ );
+
+ if (protocol === ConversationProtocol.MLS || localMLSConversation) {
+ if (localProteusConversation && localMLSConversation) {
+ await this.replaceProteus1to1WithMLS(localProteusConversation, localMLSConversation);
+ }
+ return this.initMLS1to1Conversation(userEntity, isSupportedByTheOtherUser);
+ }
+
+ const proteusConversation = await this.getProteus1To1Conversation(userEntity);
+
+ if (!proteusConversation) {
+ return null;
+ }
+
+ return this.initProteus1to1Conversation(proteusConversation.qualifiedId, isSupportedByTheOtherUser);
+ }
+
+ /**
+ * Get conversation with a user.
+ * @param userEntity User entity for whom to get the conversation
+ * @returns Resolves with the conversation with requested user
+ */
+ private async getProteus1To1Conversation(userEntity: User): Promise {
const selfUser = this.userState.self();
const inCurrentTeam = userEntity.inTeam() && !!selfUser && userEntity.teamId === selfUser.teamId;
@@ -1442,7 +1476,7 @@ export class ConversationRepository {
}
if (!mlsConversation.hasCreationMessage) {
- this.addCreationMessage(mlsConversation, this.userState.self().isTemporaryGuest());
+ this.addCreationMessage(mlsConversation, !!this.userState.self()?.isTemporaryGuest());
}
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
From 3fcf24b7e85ea2280d5e36fe6f098485b294c082 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Fri, 1 Sep 2023 13:25:53 +0200
Subject: [PATCH 27/70] runfix: do not insert group creation message
---
.../conversation/ConversationRepository.ts | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 37173e9e2dd..d7a6fa10afb 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1202,10 +1202,12 @@ export class ConversationRepository {
);
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
- if (localProteusConversation && localMLSConversation) {
- await this.replaceProteus1to1WithMLS(localProteusConversation, localMLSConversation);
+ const mlsConversation = await this.initMLS1to1Conversation(userEntity, isSupportedByTheOtherUser);
+ if (localProteusConversation) {
+ await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
}
- return this.initMLS1to1Conversation(userEntity, isSupportedByTheOtherUser);
+
+ return mlsConversation;
}
const proteusConversation = await this.getProteus1To1Conversation(userEntity);
@@ -1475,10 +1477,6 @@ export class ConversationRepository {
await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
}
- if (!mlsConversation.hasCreationMessage) {
- this.addCreationMessage(mlsConversation, !!this.userState.self()?.isTemporaryGuest());
- }
-
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
this.logger.info(`Deleting proteus 1:1 conversation ${proteusConversation.id}`);
@@ -1548,10 +1546,6 @@ export class ConversationRepository {
const otherUser = await this.userRepository.getUserById(otherUserId);
- //TODO: for team 1:1 conversation we don't need to have connection assigned
- mlsConversation.connection(otherUser.connection());
- await this.updateParticipatingUserEntities(mlsConversation);
-
//if mls is not supported by the other user we do not establish the group yet
//we just mark the mls conversation as readonly and return it
if (!isMLSSupportedByTheOtherUser) {
@@ -1570,6 +1564,9 @@ export class ConversationRepository {
this.logger.info(`MLS 1:1 conversation with user ${otherUserId.id} is established.`);
+ //TODO: for team 1:1 conversation we don't need to have connection assigned
+ mlsConversation.connection(otherUser.connection());
+ await this.updateParticipatingUserEntities(mlsConversation);
return establishedMLSConversation;
};
From 1a53274f520bb3981c7e9d3c848acdddfec9aa15 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Fri, 1 Sep 2023 17:38:32 +0200
Subject: [PATCH 28/70] feat: update participating user ids and entities after
establishing
---
src/script/conversation/ConversationRepository.ts | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index d7a6fa10afb..1048955a770 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1503,6 +1503,12 @@ export class ConversationRepository {
mlsConversation: MLSConversation,
otherUserId: QualifiedId,
): Promise => {
+ const selfUser = this.userState.self();
+
+ if (!selfUser) {
+ throw new Error('Self user is not available!');
+ }
+
const conversationService = this.core.service?.conversation;
if (!conversationService) {
@@ -1517,7 +1523,7 @@ export class ConversationRepository {
return mlsConversation;
}
- const selfUserId = this.userState.self().qualifiedId;
+ const selfUserId = selfUser.qualifiedId;
//if it's not established, establish it and add the other user to the group
await conversationService.establishMLS1to1Conversation(
@@ -1527,7 +1533,10 @@ export class ConversationRepository {
);
//refetch the conversation to get the letest epoch and updated participants list
- return this.fetchMLS1to1Conversation(otherUserId);
+ const {members, epoch} = await this.conversationService.getMLS1to1Conversation(otherUserId);
+ ConversationMapper.updateProperties(mlsConversation, {participating_user_ids: members.others, epoch});
+ await this.updateParticipatingUserEntities(mlsConversation);
+ return mlsConversation;
};
/**
@@ -1566,7 +1575,6 @@ export class ConversationRepository {
//TODO: for team 1:1 conversation we don't need to have connection assigned
mlsConversation.connection(otherUser.connection());
- await this.updateParticipatingUserEntities(mlsConversation);
return establishedMLSConversation;
};
From 30241ac8c7b97580198e1cffd8f2b97cc7251039 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 4 Sep 2023 11:39:20 +0200
Subject: [PATCH 29/70] refactor: move group establishment part to core
---
package.json | 1 +
.../conversation/ConversationRepository.ts | 19 +++----------------
2 files changed, 4 insertions(+), 16 deletions(-)
diff --git a/package.json b/package.json
index fb53379a45c..aa5785c6e38 100644
--- a/package.json
+++ b/package.json
@@ -187,6 +187,7 @@
"configure": "copy-config",
"deploy": "yarn build:prod && eb deploy",
"dev": "yarn start",
+ "dev:elna": "PORT=8082 FEDERATION=elna FEATURE_ENABLE_FEDERATION=true URL_ACCOUNT_BASE=https://account.elna.wire.link BACKEND_REST=https://nginz-https.elna.wire.link BACKEND_WS=wss://nginz-ssl.elna.wire.link FEATURE_CHECK_CONSENT=false yarn start",
"docker": "node ./bin/push_docker.js",
"zip": "node ./bin/zip.js",
"fix": "yarn fix:other && yarn fix:code",
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 1048955a770..f18867afea1 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1515,25 +1515,12 @@ export class ConversationRepository {
throw new Error('Conversation service is not available!');
}
- const {groupId, qualifiedId} = mlsConversation;
-
- //if epoch is higher that 0 it means that the group is already established, we have to join with external commit
- if (mlsConversation.epoch > 0) {
- await conversationService.joinByExternalCommit(qualifiedId);
- return mlsConversation;
- }
-
- const selfUserId = selfUser.qualifiedId;
-
- //if it's not established, establish it and add the other user to the group
- await conversationService.establishMLS1to1Conversation(
- groupId,
- {client: this.core.clientId, user: selfUserId},
+ const {members, epoch} = await conversationService.establishMLS1to1Conversation(
+ mlsConversation.groupId,
+ {client: this.core.clientId, user: selfUser.qualifiedId},
otherUserId,
);
- //refetch the conversation to get the letest epoch and updated participants list
- const {members, epoch} = await this.conversationService.getMLS1to1Conversation(otherUserId);
ConversationMapper.updateProperties(mlsConversation, {participating_user_ids: members.others, epoch});
await this.updateParticipatingUserEntities(mlsConversation);
return mlsConversation;
From d268251b4b7e0801edaa59bdc070b8af8854ef20 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 4 Sep 2023 16:25:57 +0200
Subject: [PATCH 30/70] chore: bump core
---
package.json | 1 -
1 file changed, 1 deletion(-)
diff --git a/package.json b/package.json
index aa5785c6e38..fb53379a45c 100644
--- a/package.json
+++ b/package.json
@@ -187,7 +187,6 @@
"configure": "copy-config",
"deploy": "yarn build:prod && eb deploy",
"dev": "yarn start",
- "dev:elna": "PORT=8082 FEDERATION=elna FEATURE_ENABLE_FEDERATION=true URL_ACCOUNT_BASE=https://account.elna.wire.link BACKEND_REST=https://nginz-https.elna.wire.link BACKEND_WS=wss://nginz-ssl.elna.wire.link FEATURE_CHECK_CONSENT=false yarn start",
"docker": "node ./bin/push_docker.js",
"zip": "node ./bin/zip.js",
"fix": "yarn fix:other && yarn fix:code",
From 99dc47d19e9b3bffd4cb31a94722919843491cbb Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Tue, 5 Sep 2023 15:26:16 +0200
Subject: [PATCH 31/70] runfix: is mls 1:1 already established
---
src/script/conversation/ConversationRepository.ts | 2 +-
src/script/conversation/ConversationService.ts | 9 +++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index f18867afea1..25170a567fc 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1552,7 +1552,7 @@ export class ConversationRepository {
return mlsConversation;
}
- const isAlreadyEstablished = await this.conversationService.isMLSConversationEstablished(mlsConversation.groupId);
+ const isAlreadyEstablished = await this.conversationService.isMLSGroupEstablishedLocally(mlsConversation.groupId);
const establishedMLSConversation = isAlreadyEstablished
? mlsConversation
diff --git a/src/script/conversation/ConversationService.ts b/src/script/conversation/ConversationService.ts
index faeb74ab917..3e094a702f0 100644
--- a/src/script/conversation/ConversationService.ts
+++ b/src/script/conversation/ConversationService.ts
@@ -431,6 +431,15 @@ export class ConversationService {
return this.coreConversationService.mlsGroupExistsLocally(groupId);
}
+ /**
+ * Will check if mls group is established locally.
+ * Group is established after the first commit was sent in the group and epoch number is at least 1.
+ * @param groupId groupId of the conversation
+ */
+ async isMLSGroupEstablishedLocally(groupId: string): Promise {
+ return this.coreConversationService.isMLSGroupEstablishedLocally(groupId);
+ }
+
async getMLS1to1Conversation(userId: QualifiedId) {
return this.apiClient.api.conversation.getMLS1to1Conversation(userId);
}
From 00a5d69ba2507fef2a3b3a360ce755d36c63cf92 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Tue, 5 Sep 2023 17:19:34 +0200
Subject: [PATCH 32/70] feat: add delay to the user that has received
connection accepted event
---
.../conversation/ConversationRepository.ts | 23 +++++++++++++++----
src/script/conversation/ConversationState.ts | 7 +++++-
2 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 25170a567fc..7b2c33a45dd 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -170,6 +170,7 @@ export class ConversationRepository {
return {
CONFIRMATION_THRESHOLD: TIME_IN_MILLIS.WEEK,
EXTERNAL_MESSAGE_THRESHOLD: 200 * 1024,
+ ESTABLISH_MLS_GROUP_AFTER_CONNECTION_IS_ACCEPTED_DELAY: 3000,
GROUP: {
MAX_NAME_LENGTH: 64,
MAX_SIZE: Config.getConfig().MAX_GROUP_PARTICIPANTS,
@@ -1497,9 +1498,8 @@ export class ConversationRepository {
*
* @param mlsConversation - mls 1:1 conversation
* @param otherUserId - id of the other user
- * @param proteusConversation - (optional) proteus 1:1 conversation
*/
- public readonly establishMLS1to1Conversation = async (
+ private readonly establishMLS1to1Conversation = async (
mlsConversation: MLSConversation,
otherUserId: QualifiedId,
): Promise => {
@@ -1536,6 +1536,7 @@ export class ConversationRepository {
private readonly initMLS1to1Conversation = async (
otherUserId: QualifiedId,
isMLSSupportedByTheOtherUser: boolean,
+ shouldDelayGroupEstablishment = false,
): Promise => {
this.logger.info(`Initialising MLS 1:1 conversation with user ${otherUserId.id}...`);
const mlsConversation = await this.getMLS1to1Conversation(otherUserId);
@@ -1552,6 +1553,12 @@ export class ConversationRepository {
return mlsConversation;
}
+ if (shouldDelayGroupEstablishment) {
+ await new Promise(resolve =>
+ setTimeout(resolve, ConversationRepository.CONFIG.ESTABLISH_MLS_GROUP_AFTER_CONNECTION_IS_ACCEPTED_DELAY),
+ );
+ }
+
const isAlreadyEstablished = await this.conversationService.isMLSGroupEstablishedLocally(mlsConversation.groupId);
const establishedMLSConversation = isAlreadyEstablished
@@ -1662,7 +1669,7 @@ export class ConversationRepository {
return null;
};
- private readonly getConnectionConversation = async (connectionEntity: ConnectionEntity) => {
+ private readonly getConnectionConversation = async (connectionEntity: ConnectionEntity, source?: EventSource) => {
//As of how backed works now (August 2023), proteus 1:1 conversations will always be created, even if both users support MLS conversation.
//Proteus 1:1 conversation is created right after a connection request is sent.
//Therefore, conversationId filed on connectionEntity will always indicate proteus 1:1 conversation.
@@ -1704,7 +1711,13 @@ export class ConversationRepository {
//if it's accepted, initialise conversation so it's ready to be used
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
- const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
+ const isWebSocketEvent = source === EventSource.WEBSOCKET;
+
+ const mlsConversation = await this.initMLS1to1Conversation(
+ otherUserId,
+ isSupportedByTheOtherUser,
+ isWebSocketEvent,
+ );
if (localProteusConversation && isProteusConversation(localProteusConversation)) {
await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
@@ -1734,7 +1747,7 @@ export class ConversationRepository {
source?: EventSource,
): Promise => {
try {
- const conversation = await this.getConnectionConversation(connectionEntity);
+ const conversation = await this.getConnectionConversation(connectionEntity, source);
if (!conversation) {
return undefined;
diff --git a/src/script/conversation/ConversationState.ts b/src/script/conversation/ConversationState.ts
index 309fac73100..f6ed9a4059c 100644
--- a/src/script/conversation/ConversationState.ts
+++ b/src/script/conversation/ConversationState.ts
@@ -217,13 +217,18 @@ export class ConversationState {
return false;
}
+ const connection = conversation.connection();
+ if (connection.userId) {
+ return matchQualifiedIds(connection.userId, userId);
+ }
+
if (!conversation.is1to1()) {
return false;
}
const conversationMembersIds = conversation.participating_user_ids();
const otherUserQualifiedId = conversationMembersIds.length === 1 ? conversationMembersIds[0] : null;
- const doesUserIdMatch = !!otherUserQualifiedId && otherUserQualifiedId.id === userId.id;
+ const doesUserIdMatch = !!otherUserQualifiedId && matchQualifiedIds(otherUserQualifiedId, userId);
return doesUserIdMatch;
},
From 0c0d560266459ebb872226c62f3949f51a94d367 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Wed, 6 Sep 2023 18:37:58 +0200
Subject: [PATCH 33/70] chore: add comment to establishment delay
---
src/script/conversation/ConversationRepository.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 7b2c33a45dd..03edb468a87 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1553,6 +1553,10 @@ export class ConversationRepository {
return mlsConversation;
}
+ // After connection request is accepted, both sides (users) of connection will react to conversation status update event.
+ // We want to reduce the possibility of two users trying to establish an MLS group at the same time.
+ // A user that has previously sent a connection request will wait for a short period of time before establishing an MLS group.
+ // It's very likely that this user will receive a welcome message after the user that has accepted a connection request, establishes an MLS group without any delay.
if (shouldDelayGroupEstablishment) {
await new Promise(resolve =>
setTimeout(resolve, ConversationRepository.CONFIG.ESTABLISH_MLS_GROUP_AFTER_CONNECTION_IS_ACCEPTED_DELAY),
From 3b146479000f8e8a9ff4757cf753d2663ac60dd5 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 08:58:50 +0200
Subject: [PATCH 34/70] chore: todo comment
---
src/script/conversation/ConversationRepository.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 03edb468a87..221dfbef166 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1480,9 +1480,12 @@ export class ConversationRepository {
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
+ //TODO:
+ // Before we delete the proteus 1:1 conversation, we need to make sure all the local properties are also migrated (eg. archive_state)
+ // ConversationMapper.updateProperties(mlsConversation, {});
+
this.logger.info(`Deleting proteus 1:1 conversation ${proteusConversation.id}`);
await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
- //TODO: maintain the list of retired proteus 1:1 conversations so they are not requested from backend anymore
if (isActiveConversation) {
this.logger.info(
@@ -1490,6 +1493,8 @@ export class ConversationRepository {
);
amplify.publish(WebAppEvents.CONVERSATION.SHOW, mlsConversation, {});
}
+
+ //TODO: maintain the list of retired proteus 1:1 conversations so they are not requested from backend anymore
};
/**
From d0aefcc7f70eb5cf4f77e52538d7d8cc5613210f Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 09:09:40 +0200
Subject: [PATCH 35/70] refactor: get initialised conversation
---
src/script/view_model/ContentViewModel.ts | 47 +++++++++++++----------
1 file changed, 27 insertions(+), 20 deletions(-)
diff --git a/src/script/view_model/ContentViewModel.ts b/src/script/view_model/ContentViewModel.ts
index e165cb9dd4f..573ba7ee20c 100644
--- a/src/script/view_model/ContentViewModel.ts
+++ b/src/script/view_model/ContentViewModel.ts
@@ -138,6 +138,21 @@ export class ContentViewModel {
this.conversationState.activeConversation(conversationEntity);
};
+ private readonly getConversationToDisplay = async (
+ conversation: Conversation | string,
+ domain: string | null = null,
+ ): Promise => {
+ const conversationEntity = isConversationEntity(conversation)
+ ? conversation
+ : await this.conversationRepository.getConversationById({domain: domain || '', id: conversation});
+
+ if (!conversationEntity.is1to1()) {
+ return conversationEntity;
+ }
+
+ return this.conversationRepository.init1to1Conversation(conversationEntity);
+ };
+
/**
* Opens the specified conversation.
*
@@ -167,9 +182,7 @@ export class ContentViewModel {
}
try {
- const conversationEntity = isConversationEntity(conversation)
- ? conversation
- : await this.conversationRepository.getConversationById({domain: domain || '', id: conversation});
+ const conversationEntity = await this.getConversationToDisplay(conversation, domain);
if (!conversationEntity) {
rightSidebar.close();
@@ -180,22 +193,18 @@ export class ContentViewModel {
);
}
- const initialisedConversation =
- (conversationEntity.is1to1() && (await this.conversationRepository.init1to1Conversation(conversationEntity))) ||
- conversationEntity;
-
- const isActiveConversation = this.conversationState.isActiveConversation(initialisedConversation);
+ const isActiveConversation = this.conversationState.isActiveConversation(conversationEntity);
if (!isActiveConversation) {
rightSidebar.close();
}
const isConversationState = contentState === ContentState.CONVERSATION;
- const isOpenedConversation = initialisedConversation && isActiveConversation && isConversationState;
+ const isOpenedConversation = conversationEntity && isActiveConversation && isConversationState;
if (isOpenedConversation) {
if (openNotificationSettings) {
- rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: initialisedConversation});
+ rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: conversationEntity});
}
return;
}
@@ -204,26 +213,24 @@ export class ContentViewModel {
this.mainViewModel.list.openConversations();
if (!isActiveConversation) {
- this.conversationState.activeConversation(initialisedConversation);
+ this.conversationState.activeConversation(conversationEntity);
}
- const messageEntity = openFirstSelfMention
- ? initialisedConversation.getFirstUnreadSelfMention()
- : exposeMessageEntity;
+ const messageEntity = openFirstSelfMention ? conversationEntity.getFirstUnreadSelfMention() : exposeMessageEntity;
- if (initialisedConversation.is_cleared()) {
- initialisedConversation.cleared_timestamp(0);
+ if (conversationEntity.is_cleared()) {
+ conversationEntity.cleared_timestamp(0);
}
- if (initialisedConversation.is_archived()) {
- await this.conversationRepository.unarchiveConversation(initialisedConversation);
+ if (conversationEntity.is_archived()) {
+ await this.conversationRepository.unarchiveConversation(conversationEntity);
}
- this.changeConversation(initialisedConversation, messageEntity);
+ this.changeConversation(conversationEntity, messageEntity);
this.showContent(ContentState.CONVERSATION);
this.previousConversation = this.conversationState.activeConversation();
setHistoryParam(
- generateConversationUrl({id: initialisedConversation?.id ?? '', domain: initialisedConversation?.domain ?? ''}),
+ generateConversationUrl({id: conversationEntity?.id ?? '', domain: conversationEntity?.domain ?? ''}),
);
if (openNotificationSettings) {
From 70cde62a9ab76a22446d0691bb73a146655d13d5 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 09:13:29 +0200
Subject: [PATCH 36/70] runfix: put history state back to sethistoryparam call
---
src/script/view_model/ContentViewModel.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/script/view_model/ContentViewModel.ts b/src/script/view_model/ContentViewModel.ts
index 573ba7ee20c..072793d3dc4 100644
--- a/src/script/view_model/ContentViewModel.ts
+++ b/src/script/view_model/ContentViewModel.ts
@@ -231,6 +231,7 @@ export class ContentViewModel {
this.previousConversation = this.conversationState.activeConversation();
setHistoryParam(
generateConversationUrl({id: conversationEntity?.id ?? '', domain: conversationEntity?.domain ?? ''}),
+ history.state,
);
if (openNotificationSettings) {
From 40984540d1637d565b1203afa79ff72fd589643d Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 09:53:46 +0200
Subject: [PATCH 37/70] chore: improve comments
---
.../conversation/ConversationRepository.ts | 36 +++++++++----------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 221dfbef166..a898a7faf04 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1652,17 +1652,17 @@ export class ConversationRepository {
`Protocol for 1:1 conversation ${conversation.id} with user ${otherUserId.id} is ${protocol}, isSupportedByTheOtherUser: ${isSupportedByTheOtherUser}`,
);
- //when called with mls conversation, we just initialise it
+ // When called with mls conversation, we just make sure it is initialised.
if (isMLSConversation(conversation)) {
return this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
}
- //if there's local mls conversation, return it
+ // If there's local mls conversation, we want to use it
const localMLSConversation = this.conversationState.find1to1Conversation(otherUserId, ConversationProtocol.MLS);
- //if both users support mls or mls conversation is already known, we use it
- //we never go back to proteus conversation, even if one of the users do not support mls anymore
- //(e.g. due to the change of supported protocols in team configuration)
+ // If both users support mls or mls conversation is already known, we use it
+ // we never go back to proteus conversation, even if one of the users do not support mls anymore
+ // (e.g. due to the change of supported protocols in team configuration)
if (localMLSConversation || protocol === ConversationProtocol.MLS) {
const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
if (isProteusConversation(conversation)) {
@@ -1679,38 +1679,38 @@ export class ConversationRepository {
};
private readonly getConnectionConversation = async (connectionEntity: ConnectionEntity, source?: EventSource) => {
- //As of how backed works now (August 2023), proteus 1:1 conversations will always be created, even if both users support MLS conversation.
- //Proteus 1:1 conversation is created right after a connection request is sent.
- //Therefore, conversationId filed on connectionEntity will always indicate proteus 1:1 conversation.
- //We need to manually check if mls 1:1 conversation can be used instead.
- //If mls 1:1 conversation is used, proteus 1:1 conversation will be deleted locally.
+ // As of how backed works now (August 2023), proteus 1:1 conversations will always be created, even if both users support MLS conversation.
+ // Proteus 1:1 conversation is created right after a connection request is sent.
+ // Therefore, conversationId filed on connectionEntity will always indicate proteus 1:1 conversation.
+ // We need to manually check if mls 1:1 conversation can be used instead.
+ // If mls 1:1 conversation is used, proteus 1:1 conversation will be deleted locally.
const {conversationId: proteusConversationId, userId: otherUserId} = connectionEntity;
const localProteusConversation = this.conversationState.findConversation(proteusConversationId);
if (connectionEntity.isOutgoingRequest()) {
- //return type of 3 (connect) conversation
- //it will be displayed as a connection request
+ // Return type of 3 (connect) conversation,
+ // it will be displayed as a connection request
return localProteusConversation || this.fetchConversationById(proteusConversationId);
}
const isConnectionAccepted = connectionEntity.isConnected();
- //check what protocol should be used for 1:1 conversation
+ // Check what protocol should be used for 1:1 conversation
const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(otherUserId);
const localMLSConversation = this.conversationState.find1to1Conversation(otherUserId, ConversationProtocol.MLS);
// It's not connection request and conversation is not accepted,
if (!isConnectionAccepted) {
- //if we already know mls 1:1 conversation, we use it, even if proteus protocol was now choosen as common
- //we do not support switching back to proteus after mls conversation was established
- //only proteus -> mls migration is supported, never the other way around
+ // If we already know mls 1:1 conversation, we use it, even if proteus protocol was now choosen as common,
+ // we do not support switching back to proteus after mls conversation was established,
+ // only proteus -> mls migration is supported, never the other way around.
if (!localMLSConversation) {
return protocol === ConversationProtocol.PROTEUS ? localProteusConversation : undefined;
}
- //make sure proteus conversation is gone, we don't want to see it anymore
+ // Make sure proteus conversation is gone, we don't want to see it anymore
if (localProteusConversation && isProteusConversation(localProteusConversation)) {
await this.replaceProteus1to1WithMLS(localProteusConversation, localMLSConversation);
}
@@ -1718,7 +1718,7 @@ export class ConversationRepository {
return localMLSConversation;
}
- //if it's accepted, initialise conversation so it's ready to be used
+ // If it's accepted, initialise conversation so it's ready to be used
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
const isWebSocketEvent = source === EventSource.WEBSOCKET;
From 68326e88b4a2a8181ae32f03970ef10c3045b0a4 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 10:00:18 +0200
Subject: [PATCH 38/70] chore: improve comments
---
src/script/conversation/ConversationRepository.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index a898a7faf04..debd2e16dae 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1576,7 +1576,6 @@ export class ConversationRepository {
this.logger.info(`MLS 1:1 conversation with user ${otherUserId.id} is established.`);
- //TODO: for team 1:1 conversation we don't need to have connection assigned
mlsConversation.connection(otherUser.connection());
return establishedMLSConversation;
};
@@ -1592,7 +1591,7 @@ export class ConversationRepository {
throw new Error('initProteus1to1Conversation provided with conversation id of conversation that is not proteus');
}
- //if mls is not supported by the other user we have to mark conversation as readonly
+ // If proteus is not supported by the other user we have to mark conversation as readonly
if (!doesOtherUserSupportProteus) {
//TODO: mark conversation as readonly
}
From 7ffa7f6f2937b1ef078a4b4d565ef6581102fcbf Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 10:02:49 +0200
Subject: [PATCH 39/70] chore: improve comments
---
src/script/conversation/ConversationRepository.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index debd2e16dae..a46995a26de 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -3526,6 +3526,8 @@ export class ConversationRepository {
);
const mlsConversation = await this.initMLS1to1Conversation(otherUserId, true);
+
+ // When we receive a welcome message for mls 1:1 conversation, we need to make sure proteus 1:1 with this user is deleted (if it exists)
if (proteus1to1Conversation) {
await this.replaceProteus1to1WithMLS(proteus1to1Conversation, mlsConversation);
}
From bad06de9553840158816aa2f4eb0d6aeb1e657fc Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 10:18:08 +0200
Subject: [PATCH 40/70] test: refactor get1to1conversation tests
---
.../ConversationRepository.test.ts | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts
index 71122ebb0ec..0dbc360ccd3 100644
--- a/src/script/conversation/ConversationRepository.test.ts
+++ b/src/script/conversation/ConversationRepository.test.ts
@@ -220,7 +220,10 @@ describe('ConversationRepository', () => {
describe('get1To1Conversation', () => {
beforeEach(() => testFactory.conversation_repository['conversationState'].conversations([]));
- it('finds an existing 1:1 conversation within a team', () => {
+ it('finds an existing 1:1 conversation within a team', async () => {
+ const conversationRepository = testFactory.conversation_repository!;
+ const userRepository = testFactory.user_repository!;
+
const team1to1Conversation: Partial = {
access: [CONVERSATION_ACCESS.INVITE],
creator: '109da9ca-a495-47a8-ac70-9ffbe924b2d0',
@@ -250,27 +253,27 @@ describe('ConversationRepository', () => {
const [newConversationEntity] = ConversationMapper.mapConversations([
team1to1Conversation as ConversationDatabaseData,
]);
- testFactory.conversation_repository['conversationState'].conversations.push(newConversationEntity);
+ conversationRepository['conversationState'].conversations.push(newConversationEntity);
const teamId = team1to1Conversation.team;
- const teamMemberId = team1to1Conversation.members.others[0].id;
+ const teamMemberId = team1to1Conversation.members?.others[0].id;
const userEntity = new User(teamMemberId, 'test-domain');
userEntity.inTeam(true);
userEntity.isTeamMember(true);
userEntity.teamId = teamId;
userEntity.supportedProtocols([ConversationProtocol.PROTEUS]);
- testFactory.user_repository['userState'].users.push(userEntity);
+ userRepository['userState'].users.push(userEntity);
const selfUser = generateUser();
selfUser.teamId = teamId;
selfUser.supportedProtocols([ConversationProtocol.PROTEUS]);
- spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser);
+ jest.spyOn(conversationRepository['userState'], 'self').mockReturnValue(selfUser);
- return testFactory.conversation_repository.get1To1Conversation(userEntity).then(conversationEntity => {
- expect(conversationEntity).toBe(newConversationEntity);
- });
+ const conversationEntity = await conversationRepository.get1To1Conversation(userEntity);
+
+ expect(conversationEntity).toBe(newConversationEntity);
});
});
From 19d383747171e06b731b8fc65da7b76c1cc0dd7b Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 15:19:06 +0200
Subject: [PATCH 41/70] test: get1To1Conversation
---
src/__mocks__/@wireapp/core.ts | 2 +
.../ConversationRepository.test.ts | 132 +++++++++++++++++-
.../conversation/ConversationRepository.ts | 37 +++--
3 files changed, 154 insertions(+), 17 deletions(-)
diff --git a/src/__mocks__/@wireapp/core.ts b/src/__mocks__/@wireapp/core.ts
index 533f85edb78..daa35e10ebe 100644
--- a/src/__mocks__/@wireapp/core.ts
+++ b/src/__mocks__/@wireapp/core.ts
@@ -51,6 +51,8 @@ export class Account extends EventEmitter {
mlsGroupExistsLocally: jest.fn(),
joinByExternalCommit: jest.fn(),
addUsersToMLSConversation: jest.fn(),
+ isMLSGroupEstablishedLocally: jest.fn(),
+ establishMLS1to1Conversation: jest.fn(),
messageTimer: {
setConversationLevelTimer: jest.fn(),
},
diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts
index 0dbc360ccd3..3f272447b5d 100644
--- a/src/script/conversation/ConversationRepository.test.ts
+++ b/src/script/conversation/ConversationRepository.test.ts
@@ -26,6 +26,7 @@ import {
CONVERSATION_LEGACY_ACCESS_ROLE,
CONVERSATION_TYPE,
RemoteConversations,
+ MLSConversation as BackendMLSConversation,
} 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';
@@ -53,7 +54,10 @@ import {ClientEvent} from 'src/script/event/Client';
import {EventRepository} from 'src/script/event/EventRepository';
import {NOTIFICATION_HANDLING_STATE} from 'src/script/event/NotificationHandlingState';
import {StorageSchemata} from 'src/script/storage/StorageSchemata';
-import {generateConversation as _generateConversation} from 'test/helper/ConversationGenerator';
+import {
+ generateConversation as _generateConversation,
+ generateAPIConversation,
+} from 'test/helper/ConversationGenerator';
import {escapeRegex} from 'Util/SanitizationUtil';
import {createUuid} from 'Util/uuid';
@@ -218,7 +222,20 @@ describe('ConversationRepository', () => {
});
describe('get1To1Conversation', () => {
- beforeEach(() => testFactory.conversation_repository['conversationState'].conversations([]));
+ beforeEach(() => {
+ testFactory.conversation_repository['conversationState'].conversations([]);
+ testFactory.conversation_repository['userState'].users([]);
+ jest.clearAllMocks();
+ });
+
+ beforeAll(() => {
+ jest
+ .spyOn(testFactory.conversation_repository!, 'saveConversation')
+ .mockImplementation((conversation: Conversation) => {
+ testFactory.conversation_repository['conversationState'].conversations.push(conversation);
+ return Promise.resolve(conversation);
+ });
+ });
it('finds an existing 1:1 conversation within a team', async () => {
const conversationRepository = testFactory.conversation_repository!;
@@ -275,6 +292,117 @@ describe('ConversationRepository', () => {
expect(conversationEntity).toBe(newConversationEntity);
});
+
+ it('returns proteus 1:1 conversation if one of the users does not support mls', async () => {
+ const conversationRepository = testFactory.conversation_repository!;
+ const userRepository = testFactory.user_repository!;
+
+ const otherUserId = {id: 'f718410c-3833-479d-bd80-a5df03f38414', domain: 'test-domain'};
+ const otherUser = new User(otherUserId.id, otherUserId.domain);
+ otherUser.supportedProtocols([ConversationProtocol.PROTEUS]);
+ userRepository['userState'].users.push(otherUser);
+
+ const selfUserId = {id: '109da9ca-a495-47a8-ac70-9ffbe924b2d0', domain: 'test-domain'};
+ const selfUser = new User(selfUserId.id, selfUserId.domain);
+ selfUser.supportedProtocols([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]);
+ jest.spyOn(conversationRepository['userState'], 'self').mockReturnValue(selfUser);
+
+ const proteus1to1ConversationResponse = generateAPIConversation({
+ id: {id: '04ab891e-ccf1-4dba-9d74-bacec64b5b1e', domain: 'test-domain'},
+ type: CONVERSATION_TYPE.ONE_TO_ONE,
+ protocol: ConversationProtocol.PROTEUS,
+ }) as BackendConversation;
+
+ const connection = new ConnectionEntity();
+ connection.conversationId = proteus1to1ConversationResponse.qualified_id;
+ otherUser.connection(connection);
+
+ const [proteus1to1Conversation] = conversationRepository.mapConversations([proteus1to1ConversationResponse]);
+
+ jest
+ .spyOn(conversationRepository['conversationService'], 'getConversationById')
+ .mockResolvedValueOnce(proteus1to1ConversationResponse);
+
+ const conversationEntity = await conversationRepository.get1To1Conversation(otherUser);
+
+ expect(conversationEntity?.serialize()).toEqual(proteus1to1Conversation.serialize());
+ });
+
+ it('returns established mls 1:1 conversation if both users support mls', async () => {
+ const conversationRepository = testFactory.conversation_repository!;
+ const userRepository = testFactory.user_repository!;
+
+ const otherUserId = {id: 'f718410c-3833-479d-bd80-a5df03f38414', domain: 'test-domain'};
+ const otherUser = new User(otherUserId.id, otherUserId.domain);
+ otherUser.supportedProtocols([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]);
+ userRepository['userState'].users.push(otherUser);
+
+ const selfUserId = {id: '109da9ca-a495-47a8-ac70-9ffbe924b2d0', domain: 'test-domain'};
+ const selfUser = new User(selfUserId.id, selfUserId.domain);
+ selfUser.supportedProtocols([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]);
+ jest.spyOn(conversationRepository['userState'], 'self').mockReturnValue(selfUser);
+
+ const mls1to1ConversationResponse = generateAPIConversation({
+ id: {id: '04ab891e-ccf1-4dba-9d74-bacec64b5b1e', domain: 'test-domain'},
+ type: CONVERSATION_TYPE.ONE_TO_ONE,
+ protocol: ConversationProtocol.MLS,
+ overwites: {group_id: 'groupId'},
+ }) as BackendMLSConversation;
+
+ const [mls1to1Conversation] = conversationRepository.mapConversations([mls1to1ConversationResponse]);
+
+ jest
+ .spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
+ .mockResolvedValueOnce(mls1to1ConversationResponse);
+
+ jest
+ .spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
+ .mockResolvedValueOnce(true);
+
+ const conversationEntity = await conversationRepository.get1To1Conversation(otherUser);
+
+ expect(conversationEntity?.serialize()).toEqual(mls1to1Conversation.serialize());
+ });
+
+ it('returns established mls 1:1 conversation if conversation exists locally even when proteus is choosen.', async () => {
+ const conversationRepository = testFactory.conversation_repository!;
+ const userRepository = testFactory.user_repository!;
+
+ const otherUserId = {id: 'f718410c-3833-479d-bd80-a5df03f38414', domain: 'test-domain'};
+ const otherUser = new User(otherUserId.id, otherUserId.domain);
+ otherUser.supportedProtocols([ConversationProtocol.PROTEUS]);
+ userRepository['userState'].users.push(otherUser);
+
+ const selfUserId = {id: '109da9ca-a495-47a8-ac70-9ffbe924b2d0', domain: 'test-domain'};
+ const selfUser = new User(selfUserId.id, selfUserId.domain);
+ selfUser.supportedProtocols([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]);
+ jest.spyOn(conversationRepository['userState'], 'self').mockReturnValue(selfUser);
+
+ const mls1to1ConversationResponse = generateAPIConversation({
+ id: {id: '04ab891e-ccf1-4dba-9d74-bacec64b5b1e', domain: 'test-domain'},
+ type: CONVERSATION_TYPE.ONE_TO_ONE,
+ protocol: ConversationProtocol.MLS,
+ overwites: {group_id: 'groupId'},
+ }) as BackendMLSConversation;
+
+ const [mls1to1Conversation] = conversationRepository.mapConversations([mls1to1ConversationResponse]);
+
+ const connection = new ConnectionEntity();
+ connection.conversationId = mls1to1Conversation.qualifiedId;
+ connection.userId = otherUserId;
+ otherUser.connection(connection);
+ mls1to1Conversation.connection(connection);
+
+ conversationRepository['conversationState'].conversations.push(mls1to1Conversation);
+
+ jest
+ .spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
+ .mockResolvedValueOnce(true);
+
+ const conversationEntity = await conversationRepository.get1To1Conversation(otherUser);
+
+ expect(conversationEntity?.serialize()).toEqual(mls1to1Conversation.serialize());
+ });
});
describe('getGroupsByName', () => {
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index a46995a26de..84904340fb5 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1190,7 +1190,8 @@ export class ConversationRepository {
* @returns Resolves with the conversation with requested user
*/
async get1To1Conversation(userEntity: User): Promise {
- const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(userEntity);
+ const {protocol, isMLSSupportedByTheOtherUser, isProteusSupportedByTheOtherUser} =
+ await this.getProtocolFor1to1Conversation(userEntity);
const localMLSConversation = this.conversationState.find1to1Conversation(
userEntity.qualifiedId,
@@ -1203,7 +1204,7 @@ export class ConversationRepository {
);
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
- const mlsConversation = await this.initMLS1to1Conversation(userEntity, isSupportedByTheOtherUser);
+ const mlsConversation = await this.initMLS1to1Conversation(userEntity, isMLSSupportedByTheOtherUser);
if (localProteusConversation) {
await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
}
@@ -1217,7 +1218,7 @@ export class ConversationRepository {
return null;
}
- return this.initProteus1to1Conversation(proteusConversation.qualifiedId, isSupportedByTheOtherUser);
+ return this.initProteus1to1Conversation(proteusConversation.qualifiedId, isProteusSupportedByTheOtherUser);
}
/**
@@ -1395,21 +1396,25 @@ export class ConversationRepository {
otherUserId: QualifiedId,
): Promise<{
protocol: ConversationProtocol.PROTEUS | ConversationProtocol.MLS;
- isSupportedByTheOtherUser: boolean;
+ isMLSSupportedByTheOtherUser: boolean;
+ isProteusSupportedByTheOtherUser: boolean;
}> => {
const otherUserSupportedProtocols = await this.userRepository.getUserSupportedProtocols(otherUserId);
const selfUserSupportedProtocols = await this.selfRepository.getSelfSupportedProtocols();
+ const isMLSSupportedByTheOtherUser = otherUserSupportedProtocols.includes(ConversationProtocol.MLS);
+ const isProteusSupportedByTheOtherUser = otherUserSupportedProtocols.includes(ConversationProtocol.PROTEUS);
+
const commonProtocols = otherUserSupportedProtocols.filter(protocol =>
selfUserSupportedProtocols.includes(protocol),
);
if (commonProtocols.includes(ConversationProtocol.MLS)) {
- return {protocol: ConversationProtocol.MLS, isSupportedByTheOtherUser: true};
+ return {protocol: ConversationProtocol.MLS, isMLSSupportedByTheOtherUser, isProteusSupportedByTheOtherUser};
}
if (commonProtocols.includes(ConversationProtocol.PROTEUS)) {
- return {protocol: ConversationProtocol.PROTEUS, isSupportedByTheOtherUser: true};
+ return {protocol: ConversationProtocol.PROTEUS, isMLSSupportedByTheOtherUser, isProteusSupportedByTheOtherUser};
}
//if common protocol can't be found, we use preferred protocol of the self user
@@ -1417,7 +1422,7 @@ export class ConversationRepository {
? ConversationProtocol.MLS
: ConversationProtocol.PROTEUS;
- return {protocol: preferredProtocol, isSupportedByTheOtherUser: false};
+ return {protocol: preferredProtocol, isMLSSupportedByTheOtherUser, isProteusSupportedByTheOtherUser};
};
/**
@@ -1646,14 +1651,15 @@ export class ConversationRepository {
`Initialising 1:1 conversation ${conversation.id} of type ${conversation.type()} with user ${otherUserId.id}`,
);
- const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(otherUserId);
+ const {protocol, isMLSSupportedByTheOtherUser, isProteusSupportedByTheOtherUser} =
+ await this.getProtocolFor1to1Conversation(otherUserId);
this.logger.info(
`Protocol for 1:1 conversation ${conversation.id} with user ${otherUserId.id} is ${protocol}, isSupportedByTheOtherUser: ${isSupportedByTheOtherUser}`,
);
// When called with mls conversation, we just make sure it is initialised.
if (isMLSConversation(conversation)) {
- return this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
+ return this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser);
}
// If there's local mls conversation, we want to use it
@@ -1662,8 +1668,8 @@ export class ConversationRepository {
// If both users support mls or mls conversation is already known, we use it
// we never go back to proteus conversation, even if one of the users do not support mls anymore
// (e.g. due to the change of supported protocols in team configuration)
- if (localMLSConversation || protocol === ConversationProtocol.MLS) {
- const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isSupportedByTheOtherUser);
+ if (protocol === ConversationProtocol.MLS || localMLSConversation) {
+ const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser);
if (isProteusConversation(conversation)) {
await this.replaceProteus1to1WithMLS(conversation, mlsConversation);
}
@@ -1671,7 +1677,7 @@ export class ConversationRepository {
}
if (protocol === ConversationProtocol.PROTEUS && isProteusConversation(conversation)) {
- return this.initProteus1to1Conversation(conversation, isSupportedByTheOtherUser);
+ return this.initProteus1to1Conversation(conversation, isProteusSupportedByTheOtherUser);
}
return null;
@@ -1696,7 +1702,8 @@ export class ConversationRepository {
const isConnectionAccepted = connectionEntity.isConnected();
// Check what protocol should be used for 1:1 conversation
- const {protocol, isSupportedByTheOtherUser} = await this.getProtocolFor1to1Conversation(otherUserId);
+ const {protocol, isMLSSupportedByTheOtherUser, isProteusSupportedByTheOtherUser} =
+ await this.getProtocolFor1to1Conversation(otherUserId);
const localMLSConversation = this.conversationState.find1to1Conversation(otherUserId, ConversationProtocol.MLS);
@@ -1723,7 +1730,7 @@ export class ConversationRepository {
const mlsConversation = await this.initMLS1to1Conversation(
otherUserId,
- isSupportedByTheOtherUser,
+ isMLSSupportedByTheOtherUser,
isWebSocketEvent,
);
@@ -1735,7 +1742,7 @@ export class ConversationRepository {
}
if (protocol === ConversationProtocol.PROTEUS) {
- return this.initProteus1to1Conversation(proteusConversationId, isSupportedByTheOtherUser);
+ return this.initProteus1to1Conversation(proteusConversationId, isProteusSupportedByTheOtherUser);
}
return undefined;
From e66b8c9b798156b35e3042c9a2b0b39ee54560a7 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 15:37:09 +0200
Subject: [PATCH 42/70] chore: fix log
---
src/script/conversation/ConversationRepository.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 84904340fb5..d4432ba0468 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1654,7 +1654,10 @@ export class ConversationRepository {
const {protocol, isMLSSupportedByTheOtherUser, isProteusSupportedByTheOtherUser} =
await this.getProtocolFor1to1Conversation(otherUserId);
this.logger.info(
- `Protocol for 1:1 conversation ${conversation.id} with user ${otherUserId.id} is ${protocol}, isSupportedByTheOtherUser: ${isSupportedByTheOtherUser}`,
+ `Protocol for 1:1 conversation ${conversation.id} with user ${otherUserId.id} is ${protocol}, ${JSON.stringify({
+ isMLSSupportedByTheOtherUser,
+ isProteusSupportedByTheOtherUser,
+ })}`,
);
// When called with mls conversation, we just make sure it is initialised.
From d9f6847306f5a7f0b38aaad81926afbcc3db1823 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 16:32:07 +0200
Subject: [PATCH 43/70] refactor: replace proteus as a part of init mls
---
.../conversation/ConversationRepository.ts | 89 +++++++------------
1 file changed, 31 insertions(+), 58 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index d4432ba0468..f1c5e81bf21 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1198,18 +1198,8 @@ export class ConversationRepository {
ConversationProtocol.MLS,
);
- const localProteusConversation = this.conversationState.find1to1Conversation(
- userEntity.qualifiedId,
- ConversationProtocol.PROTEUS,
- );
-
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
- const mlsConversation = await this.initMLS1to1Conversation(userEntity, isMLSSupportedByTheOtherUser);
- if (localProteusConversation) {
- await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
- }
-
- return mlsConversation;
+ return this.initMLS1to1Conversation(userEntity, isMLSSupportedByTheOtherUser);
}
const proteusConversation = await this.getProteus1To1Conversation(userEntity);
@@ -1582,6 +1572,17 @@ export class ConversationRepository {
this.logger.info(`MLS 1:1 conversation with user ${otherUserId.id} is established.`);
mlsConversation.connection(otherUser.connection());
+
+ const localProteusConversation = this.conversationState.find1to1Conversation(
+ otherUserId,
+ ConversationProtocol.PROTEUS,
+ );
+
+ // If proteus 1:1 conversation with the same user is known, we have to make sure it is replaced with mls 1:1 conversation
+ if (localProteusConversation) {
+ await this.replaceProteus1to1WithMLS(localProteusConversation, establishedMLSConversation);
+ }
+
return establishedMLSConversation;
};
@@ -1672,11 +1673,7 @@ export class ConversationRepository {
// we never go back to proteus conversation, even if one of the users do not support mls anymore
// (e.g. due to the change of supported protocols in team configuration)
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
- const mlsConversation = await this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser);
- if (isProteusConversation(conversation)) {
- await this.replaceProteus1to1WithMLS(conversation, mlsConversation);
- }
- return mlsConversation;
+ return this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser);
}
if (protocol === ConversationProtocol.PROTEUS && isProteusConversation(conversation)) {
@@ -1696,9 +1693,8 @@ export class ConversationRepository {
const {conversationId: proteusConversationId, userId: otherUserId} = connectionEntity;
const localProteusConversation = this.conversationState.findConversation(proteusConversationId);
+ // For connection request, we simply display proteus conversation of type 3 (connect) it will be displayed as a connection request
if (connectionEntity.isOutgoingRequest()) {
- // Return type of 3 (connect) conversation,
- // it will be displayed as a connection request
return localProteusConversation || this.fetchConversationById(proteusConversationId);
}
@@ -1710,45 +1706,32 @@ export class ConversationRepository {
const localMLSConversation = this.conversationState.find1to1Conversation(otherUserId, ConversationProtocol.MLS);
- // It's not connection request and conversation is not accepted,
- if (!isConnectionAccepted) {
- // If we already know mls 1:1 conversation, we use it, even if proteus protocol was now choosen as common,
- // we do not support switching back to proteus after mls conversation was established,
- // only proteus -> mls migration is supported, never the other way around.
- if (!localMLSConversation) {
- return protocol === ConversationProtocol.PROTEUS ? localProteusConversation : undefined;
+ // If it's accepted, initialise conversation so it's ready to be used
+ if (isConnectionAccepted) {
+ if (protocol === ConversationProtocol.MLS || localMLSConversation) {
+ const isWebSocketEvent = source === EventSource.WEBSOCKET;
+ return this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser, isWebSocketEvent);
}
- // Make sure proteus conversation is gone, we don't want to see it anymore
- if (localProteusConversation && isProteusConversation(localProteusConversation)) {
- await this.replaceProteus1to1WithMLS(localProteusConversation, localMLSConversation);
+ if (protocol === ConversationProtocol.PROTEUS) {
+ return this.initProteus1to1Conversation(proteusConversationId, isProteusSupportedByTheOtherUser);
}
-
- return localMLSConversation;
}
- // If it's accepted, initialise conversation so it's ready to be used
- if (protocol === ConversationProtocol.MLS || localMLSConversation) {
- const isWebSocketEvent = source === EventSource.WEBSOCKET;
-
- const mlsConversation = await this.initMLS1to1Conversation(
- otherUserId,
- isMLSSupportedByTheOtherUser,
- isWebSocketEvent,
- );
-
+ // It's not connection request and conversation is not accepted, we never fetch the conversation from backend
+ // If we already know mls 1:1 conversation, we use it, even if proteus protocol was now choosen as common,
+ // we do not support switching back to proteus after mls conversation was established,
+ // only proteus -> mls migration is supported, never the other way around.
+ if (localMLSConversation) {
+ // Make sure proteus conversation is gone, we don't want to display it anymore
if (localProteusConversation && isProteusConversation(localProteusConversation)) {
- await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
+ await this.replaceProteus1to1WithMLS(localProteusConversation, localMLSConversation);
}
- return mlsConversation;
- }
-
- if (protocol === ConversationProtocol.PROTEUS) {
- return this.initProteus1to1Conversation(proteusConversationId, isProteusSupportedByTheOtherUser);
+ return localMLSConversation;
}
- return undefined;
+ return protocol === ConversationProtocol.PROTEUS ? localProteusConversation : undefined;
};
/**
@@ -3530,17 +3513,7 @@ export class ConversationRepository {
return;
}
- const proteus1to1Conversation = this.conversationState.find1to1Conversation(
- otherUserId,
- ConversationProtocol.PROTEUS,
- );
-
- const mlsConversation = await this.initMLS1to1Conversation(otherUserId, true);
-
- // When we receive a welcome message for mls 1:1 conversation, we need to make sure proteus 1:1 with this user is deleted (if it exists)
- if (proteus1to1Conversation) {
- await this.replaceProteus1to1WithMLS(proteus1to1Conversation, mlsConversation);
- }
+ await this.initMLS1to1Conversation(otherUserId, true);
}
/**
From 5fa9e812da547cab0e9d69a879aa8528199cacc1 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Thu, 7 Sep 2023 16:58:40 +0200
Subject: [PATCH 44/70] refactor: simplify mls init for unconnected connections
---
src/script/conversation/ConversationRepository.ts | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index f1c5e81bf21..a59ddf18516 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1698,6 +1698,7 @@ export class ConversationRepository {
return localProteusConversation || this.fetchConversationById(proteusConversationId);
}
+ const isWebSocketEvent = source === EventSource.WEBSOCKET;
const isConnectionAccepted = connectionEntity.isConnected();
// Check what protocol should be used for 1:1 conversation
@@ -1709,7 +1710,6 @@ export class ConversationRepository {
// If it's accepted, initialise conversation so it's ready to be used
if (isConnectionAccepted) {
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
- const isWebSocketEvent = source === EventSource.WEBSOCKET;
return this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser, isWebSocketEvent);
}
@@ -1723,12 +1723,7 @@ export class ConversationRepository {
// we do not support switching back to proteus after mls conversation was established,
// only proteus -> mls migration is supported, never the other way around.
if (localMLSConversation) {
- // Make sure proteus conversation is gone, we don't want to display it anymore
- if (localProteusConversation && isProteusConversation(localProteusConversation)) {
- await this.replaceProteus1to1WithMLS(localProteusConversation, localMLSConversation);
- }
-
- return localMLSConversation;
+ return this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser, isWebSocketEvent);
}
return protocol === ConversationProtocol.PROTEUS ? localProteusConversation : undefined;
From 1adea1f3cb044329daf0cff5cc2c5a30bcb33ae9 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Fri, 8 Sep 2023 11:30:40 +0200
Subject: [PATCH 45/70] refactor: pass quid instead of the whole user entity
---
src/script/conversation/ConversationRepository.ts | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index a59ddf18516..51746629fe1 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1190,16 +1190,15 @@ export class ConversationRepository {
* @returns Resolves with the conversation with requested user
*/
async get1To1Conversation(userEntity: User): Promise {
+ const {qualifiedId: otherUserId} = userEntity;
+
const {protocol, isMLSSupportedByTheOtherUser, isProteusSupportedByTheOtherUser} =
- await this.getProtocolFor1to1Conversation(userEntity);
+ await this.getProtocolFor1to1Conversation(otherUserId);
- const localMLSConversation = this.conversationState.find1to1Conversation(
- userEntity.qualifiedId,
- ConversationProtocol.MLS,
- );
+ const localMLSConversation = this.conversationState.find1to1Conversation(otherUserId, ConversationProtocol.MLS);
if (protocol === ConversationProtocol.MLS || localMLSConversation) {
- return this.initMLS1to1Conversation(userEntity, isMLSSupportedByTheOtherUser);
+ return this.initMLS1to1Conversation(otherUserId, isMLSSupportedByTheOtherUser);
}
const proteusConversation = await this.getProteus1To1Conversation(userEntity);
From cfe78c3336b92387bfda51873de12c6d9ed6aca6 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Fri, 8 Sep 2023 12:56:15 +0200
Subject: [PATCH 46/70] feat: migrate conversation's local properties
---
.../conversation/ConversationRepository.ts | 35 +++++++++++++++++--
src/script/entity/Conversation.ts | 2 +-
2 files changed, 33 insertions(+), 4 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 51746629fe1..57ef29f9ba9 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1474,9 +1474,38 @@ export class ConversationRepository {
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
- //TODO:
- // Before we delete the proteus 1:1 conversation, we need to make sure all the local properties are also migrated (eg. archive_state)
- // ConversationMapper.updateProperties(mlsConversation, {});
+ // Before we delete the proteus 1:1 conversation, we need to make sure all the local properties are also migrated
+ const {
+ archivedState,
+ archivedTimestamp,
+ cleared_timestamp,
+ localMessageTimer,
+ last_event_timestamp,
+ last_read_timestamp,
+ last_server_timestamp,
+ legalHoldStatus,
+ mutedState,
+ mutedTimestamp,
+ status,
+ verification_state,
+ } = proteusConversation;
+
+ const updates: Partial> = {
+ archivedState,
+ archivedTimestamp,
+ cleared_timestamp,
+ localMessageTimer,
+ last_event_timestamp,
+ last_read_timestamp,
+ last_server_timestamp,
+ legalHoldStatus,
+ mutedState,
+ mutedTimestamp,
+ status,
+ verification_state,
+ };
+
+ ConversationMapper.updateProperties(mlsConversation, updates);
this.logger.info(`Deleting proteus 1:1 conversation ${proteusConversation.id}`);
await this.deleteConversationLocally(proteusConversation.qualifiedId, true);
diff --git a/src/script/entity/Conversation.ts b/src/script/entity/Conversation.ts
index 98b5d0b03d2..32ac570a834 100644
--- a/src/script/entity/Conversation.ts
+++ b/src/script/entity/Conversation.ts
@@ -90,7 +90,7 @@ export class Conversation {
public readonly last_server_timestamp: ko.Observable;
private readonly logger: Logger;
public readonly mutedState: ko.Observable;
- private readonly mutedTimestamp: ko.Observable;
+ public readonly mutedTimestamp: ko.Observable;
private readonly publishPersistState: (() => void) & Cancelable;
private shouldPersistStateChanges: boolean;
public blockLegalHoldMessage: boolean;
From 13f8dc63590057deda846820a8f7a86932143000 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 11 Sep 2023 08:45:23 +0200
Subject: [PATCH 47/70] refactor: move establishment delay to establish method
---
.../conversation/ConversationRepository.ts | 55 +++++++++++--------
1 file changed, 32 insertions(+), 23 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 57ef29f9ba9..d786c25a0f3 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1530,6 +1530,7 @@ export class ConversationRepository {
private readonly establishMLS1to1Conversation = async (
mlsConversation: MLSConversation,
otherUserId: QualifiedId,
+ shouldDelayGroupEstablishment = false,
): Promise => {
const selfUser = this.userState.self();
@@ -1543,12 +1544,31 @@ export class ConversationRepository {
throw new Error('Conversation service is not available!');
}
+ // After connection request is accepted, both sides (users) of connection will react to conversation status update event.
+ // We want to reduce the possibility of two users trying to establish an MLS group at the same time.
+ // A user that has previously sent a connection request will wait for a short period of time before establishing an MLS group.
+ // It's very likely that this user will receive a welcome message after the user that has accepted a connection request, establishes an MLS group without any delay.
+ if (shouldDelayGroupEstablishment) {
+ await new Promise(resolve =>
+ setTimeout(resolve, ConversationRepository.CONFIG.ESTABLISH_MLS_GROUP_AFTER_CONNECTION_IS_ACCEPTED_DELAY),
+ );
+ }
+
+ const isAlreadyEstablished = await this.conversationService.isMLSGroupEstablishedLocally(mlsConversation.groupId);
+
+ if (isAlreadyEstablished) {
+ this.logger.info(`MLS 1:1 conversation with user ${otherUserId.id} is already established.`);
+ return mlsConversation;
+ }
+
const {members, epoch} = await conversationService.establishMLS1to1Conversation(
mlsConversation.groupId,
{client: this.core.clientId, user: selfUser.qualifiedId},
otherUserId,
);
+ this.logger.info(`MLS 1:1 conversation with user ${otherUserId.id} was established.`);
+
ConversationMapper.updateProperties(mlsConversation, {participating_user_ids: members.others, epoch});
await this.updateParticipatingUserEntities(mlsConversation);
return mlsConversation;
@@ -1568,8 +1588,13 @@ export class ConversationRepository {
): Promise => {
this.logger.info(`Initialising MLS 1:1 conversation with user ${otherUserId.id}...`);
const mlsConversation = await this.getMLS1to1Conversation(otherUserId);
+ const localProteusConversation = this.conversationState.find1to1Conversation(
+ otherUserId,
+ ConversationProtocol.PROTEUS,
+ );
const otherUser = await this.userRepository.getUserById(otherUserId);
+ mlsConversation.connection(otherUser.connection());
//if mls is not supported by the other user we do not establish the group yet
//we just mark the mls conversation as readonly and return it
@@ -1578,35 +1603,19 @@ export class ConversationRepository {
this.logger.info(
`MLS 1:1 conversation with user ${otherUserId.id} is not supported by the other user, conversation will become readonly`,
);
+ // If proteus 1:1 conversation with the same user is known, we have to make sure it is replaced with mls 1:1 conversation
+ if (localProteusConversation) {
+ await this.replaceProteus1to1WithMLS(localProteusConversation, mlsConversation);
+ }
return mlsConversation;
}
- // After connection request is accepted, both sides (users) of connection will react to conversation status update event.
- // We want to reduce the possibility of two users trying to establish an MLS group at the same time.
- // A user that has previously sent a connection request will wait for a short period of time before establishing an MLS group.
- // It's very likely that this user will receive a welcome message after the user that has accepted a connection request, establishes an MLS group without any delay.
- if (shouldDelayGroupEstablishment) {
- await new Promise(resolve =>
- setTimeout(resolve, ConversationRepository.CONFIG.ESTABLISH_MLS_GROUP_AFTER_CONNECTION_IS_ACCEPTED_DELAY),
- );
- }
-
- const isAlreadyEstablished = await this.conversationService.isMLSGroupEstablishedLocally(mlsConversation.groupId);
-
- const establishedMLSConversation = isAlreadyEstablished
- ? mlsConversation
- : await this.establishMLS1to1Conversation(mlsConversation, otherUserId);
-
- this.logger.info(`MLS 1:1 conversation with user ${otherUserId.id} is established.`);
-
- mlsConversation.connection(otherUser.connection());
-
- const localProteusConversation = this.conversationState.find1to1Conversation(
+ const establishedMLSConversation = await this.establishMLS1to1Conversation(
+ mlsConversation,
otherUserId,
- ConversationProtocol.PROTEUS,
+ shouldDelayGroupEstablishment,
);
- // If proteus 1:1 conversation with the same user is known, we have to make sure it is replaced with mls 1:1 conversation
if (localProteusConversation) {
await this.replaceProteus1to1WithMLS(localProteusConversation, establishedMLSConversation);
}
From 27f0dd2871997c78fe6d9943f200e1e0e9b2703b Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 11 Sep 2023 11:27:51 +0200
Subject: [PATCH 48/70] test: establish 1:1 mls conversation
---
.../ConversationRepository.test.ts | 70 ++++++++++++++++++-
.../conversation/ConversationRepository.ts | 5 +-
2 files changed, 73 insertions(+), 2 deletions(-)
diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts
index 3f272447b5d..0c389c6135d 100644
--- a/src/script/conversation/ConversationRepository.test.ts
+++ b/src/script/conversation/ConversationRepository.test.ts
@@ -261,7 +261,7 @@ describe('ConversationRepository', () => {
status_time: '1970-01-01T00:00:00.000Z',
},
},
- name: null,
+ name: '',
protocol: ConversationProtocol.PROTEUS,
team: 'cf162e22-20b8-4533-a5ab-d3f5dde39d2c',
type: 0,
@@ -364,6 +364,72 @@ describe('ConversationRepository', () => {
expect(conversationEntity?.serialize()).toEqual(mls1to1Conversation.serialize());
});
+ it("establishes MLS 1:1 conversation if it's not yet established", async () => {
+ const conversationRepository = testFactory.conversation_repository!;
+ const userRepository = testFactory.user_repository!;
+ const mockedGroupId = 'groupId';
+
+ const otherUserId = {id: 'f718410c-3833-479d-bd80-a5df03f38414', domain: 'test-domain'};
+ const otherUser = new User(otherUserId.id, otherUserId.domain);
+ otherUser.supportedProtocols([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]);
+ userRepository['userState'].users.push(otherUser);
+
+ const mockSelfClientId = 'client-id';
+ const selfUserId = {id: '109da9ca-a495-47a8-ac70-9ffbe924b2d0', domain: 'test-domain'};
+ const selfUser = new User(selfUserId.id, selfUserId.domain);
+ selfUser.supportedProtocols([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]);
+ jest.spyOn(conversationRepository['userState'], 'self').mockReturnValue(selfUser);
+
+ const mls1to1ConversationResponse = generateAPIConversation({
+ id: {id: '04ab891e-ccf1-4dba-9d74-bacec64b5b1e', domain: 'test-domain'},
+ type: CONVERSATION_TYPE.ONE_TO_ONE,
+ protocol: ConversationProtocol.MLS,
+ overwites: {group_id: mockedGroupId},
+ }) as BackendMLSConversation;
+
+ jest
+ .spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
+ .mockResolvedValueOnce(mls1to1ConversationResponse);
+
+ jest
+ .spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
+ .mockResolvedValueOnce(false);
+
+ const establishedMls1to1ConversationResponse = generateAPIConversation({
+ id: {id: '04ab891e-ccf1-4dba-9d74-bacec64b5b1e', domain: 'test-domain'},
+ type: CONVERSATION_TYPE.ONE_TO_ONE,
+ protocol: ConversationProtocol.MLS,
+ overwites: {
+ group_id: mockedGroupId,
+ epoch: 1,
+ qualified_others: [otherUserId],
+ members: {
+ others: [{id: otherUserId.id, status: 0, qualified_id: otherUserId}],
+ } as any,
+ },
+ }) as BackendMLSConversation;
+
+ jest
+ .spyOn(container.resolve(Core).service!.conversation, 'establishMLS1to1Conversation')
+ .mockResolvedValueOnce(establishedMls1to1ConversationResponse);
+
+ Object.defineProperty(container.resolve(Core), 'clientId', {
+ get: jest.fn(() => mockSelfClientId),
+ configurable: true,
+ });
+
+ const [mls1to1Conversation] = conversationRepository.mapConversations([establishedMls1to1ConversationResponse]);
+
+ const conversationEntity = await conversationRepository.get1To1Conversation(otherUser);
+
+ expect(container.resolve(Core).service!.conversation.establishMLS1to1Conversation).toHaveBeenCalledWith(
+ mockedGroupId,
+ {client: mockSelfClientId, user: selfUserId},
+ otherUserId,
+ );
+ expect(conversationEntity?.serialize()).toEqual(mls1to1Conversation.serialize());
+ });
+
it('returns established mls 1:1 conversation if conversation exists locally even when proteus is choosen.', async () => {
const conversationRepository = testFactory.conversation_repository!;
const userRepository = testFactory.user_repository!;
@@ -946,6 +1012,7 @@ describe('ConversationRepository', () => {
Object.defineProperty(container.resolve(Core), 'clientId', {
get: jest.fn(() => mockSelfClientId),
+ configurable: true,
});
jest.spyOn(container.resolve(Core).service!.mls!, 'conversationExists').mockResolvedValueOnce(true);
@@ -1666,6 +1733,7 @@ describe('ConversationRepository', () => {
it('should call loadMissingConversations & refreshAllConversationsUnavailableParticipants every 3 hours for federated envs', async () => {
Object.defineProperty(container.resolve(Core).backendFeatures, 'isFederated', {
get: jest.fn(() => true),
+ configurable: true,
});
const conversationRepo = await testFactory.exposeConversationActors();
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index d786c25a0f3..2d08b7734b3 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1569,8 +1569,11 @@ export class ConversationRepository {
this.logger.info(`MLS 1:1 conversation with user ${otherUserId.id} was established.`);
- ConversationMapper.updateProperties(mlsConversation, {participating_user_ids: members.others, epoch});
+ const otherMembers = members.others.map(other => ({domain: other.qualified_id?.domain || '', id: other.id}));
+
+ ConversationMapper.updateProperties(mlsConversation, {participating_user_ids: otherMembers, epoch});
await this.updateParticipatingUserEntities(mlsConversation);
+
return mlsConversation;
};
From df0cfab1d0490edd5ef60951bd3d6dc408fc4da7 Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 11 Sep 2023 12:56:17 +0200
Subject: [PATCH 49/70] test: replacing proteus 1:1 with mls 1:1
---
src/script/conversation/ConversationMapper.ts | 5 ++
.../ConversationRepository.test.ts | 80 +++++++++++++++++++
.../conversation/ConversationRepository.ts | 31 ++++---
3 files changed, 99 insertions(+), 17 deletions(-)
diff --git a/src/script/conversation/ConversationMapper.ts b/src/script/conversation/ConversationMapper.ts
index 2a3a94fc150..41aeb9a0b6b 100644
--- a/src/script/conversation/ConversationMapper.ts
+++ b/src/script/conversation/ConversationMapper.ts
@@ -130,6 +130,7 @@ export class ConversationMapper {
receipt_mode,
status,
verification_state,
+ archived_state,
} = selfState;
if (archived_timestamp) {
@@ -178,6 +179,10 @@ export class ConversationMapper {
conversationEntity.verification_state(verification_state);
}
+ if (archived_state !== undefined) {
+ conversationEntity.archivedState(archived_state);
+ }
+
if (legal_hold_status) {
conversationEntity.legalHoldStatus(legal_hold_status);
}
diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts
index 0c389c6135d..472dd47c3c5 100644
--- a/src/script/conversation/ConversationRepository.test.ts
+++ b/src/script/conversation/ConversationRepository.test.ts
@@ -66,6 +66,7 @@ import {ConversationRepository} from './ConversationRepository';
import {entities, payload} from '../../../test/api/payloads';
import {TestFactory} from '../../../test/helper/TestFactory';
import {generateUser} from '../../../test/helper/UserGenerator';
+import {NOTIFICATION_STATE} from '../conversation/NotificationSetting';
import {Core} from '../service/CoreSingleton';
import {LegacyEventRecord, StorageService} from '../storage';
@@ -364,6 +365,85 @@ describe('ConversationRepository', () => {
expect(conversationEntity?.serialize()).toEqual(mls1to1Conversation.serialize());
});
+ it('replaces proteus 1:1 with mls 1:1', async () => {
+ const conversationRepository = testFactory.conversation_repository!;
+ const userRepository = testFactory.user_repository!;
+
+ const otherUserId = {id: 'f718410c-3833-479d-bd80-a5df03f38414', domain: 'test-domain'};
+ const otherUser = new User(otherUserId.id, otherUserId.domain);
+ otherUser.supportedProtocols([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]);
+ userRepository['userState'].users.push(otherUser);
+
+ const selfUserId = {id: '109da9ca-a495-47a8-ac70-9ffbe924b2d0', domain: 'test-domain'};
+ const selfUser = new User(selfUserId.id, selfUserId.domain);
+ selfUser.supportedProtocols([ConversationProtocol.PROTEUS, ConversationProtocol.MLS]);
+ jest.spyOn(conversationRepository['userState'], 'self').mockReturnValue(selfUser);
+
+ const mls1to1ConversationResponse = generateAPIConversation({
+ id: {id: '04ab891e-ccf1-4dba-9d74-bacec64b5b1e', domain: 'test-domain'},
+ type: CONVERSATION_TYPE.ONE_TO_ONE,
+ protocol: ConversationProtocol.MLS,
+ overwites: {group_id: 'groupId', archived_state: false, muted_state: NOTIFICATION_STATE.NOTHING},
+ }) as BackendMLSConversation;
+
+ const proteus1to1ConversationResponse = generateAPIConversation({
+ id: {id: '04ab891e-ccf1-4dba-9d74-bacec64b123e', domain: 'test-domain'},
+ type: CONVERSATION_TYPE.ONE_TO_ONE,
+ protocol: ConversationProtocol.PROTEUS,
+ overwites: {archived_state: true, muted_state: NOTIFICATION_STATE.EVERYTHING},
+ }) as BackendMLSConversation;
+
+ const [mls1to1Conversation, proteus1to1Conversation] = conversationRepository.mapConversations([
+ mls1to1ConversationResponse,
+ proteus1to1ConversationResponse,
+ ]);
+
+ const connection = new ConnectionEntity();
+ connection.conversationId = mls1to1Conversation.qualifiedId;
+ connection.userId = otherUserId;
+
+ otherUser.connection(connection);
+ mls1to1Conversation.connection(connection);
+ proteus1to1Conversation.connection(connection);
+
+ conversationRepository['conversationState'].conversations.push(mls1to1Conversation, proteus1to1Conversation);
+
+ jest
+ .spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
+ .mockResolvedValueOnce(mls1to1ConversationResponse);
+
+ jest
+ .spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
+ .mockResolvedValueOnce(true);
+
+ jest.spyOn(conversationRepository['eventService'], 'moveEventsToConversation');
+ jest
+ .spyOn(conversationRepository['conversationState'], 'activeConversation')
+ .mockReturnValue(proteus1to1Conversation);
+ jest.spyOn(conversationRepository['conversationService'], 'deleteConversationFromDb');
+
+ const conversationEntity = await conversationRepository.get1To1Conversation(otherUser);
+
+ expect(conversationRepository['eventService'].moveEventsToConversation).toHaveBeenCalledWith(
+ proteus1to1Conversation.id,
+ mls1to1Conversation.id,
+ );
+
+ expect(conversationEntity?.serialize()).toEqual(mls1to1Conversation.serialize());
+
+ //Local properties were migrated from proteus to mls conversation
+ expect(conversationEntity?.serialize().archived_state).toEqual(proteus1to1Conversation.archivedState());
+ expect(conversationEntity?.serialize().muted_state).toEqual(proteus1to1Conversation.mutedState());
+
+ //proteus conversation was deleted from the local store
+ expect(conversationRepository['conversationService'].deleteConversationFromDb).toHaveBeenCalledWith(
+ proteus1to1Conversation.id,
+ );
+ expect(conversationRepository['conversationState'].conversations()).not.toEqual(
+ expect.arrayContaining([proteus1to1Conversation]),
+ );
+ });
+
it("establishes MLS 1:1 conversation if it's not yet established", async () => {
const conversationRepository = testFactory.conversation_repository!;
const userRepository = testFactory.user_repository!;
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index 2d08b7734b3..1a484f456f2 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -1461,16 +1461,13 @@ export class ConversationRepository {
private readonly replaceProteus1to1WithMLS = async (
proteusConversation: ProteusConversation,
mlsConversation: MLSConversation,
- shouldMoveEvents = true,
) => {
this.logger.info(
`Replacing proteus 1:1 conversation ${proteusConversation.id} with mls 1:1 conversation ${mlsConversation.id}`,
);
- if (shouldMoveEvents) {
- this.logger.info('Moving events from proteus 1:1 conversation to MLS 1:1 conversation');
- await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
- }
+ this.logger.info('Moving events from proteus 1:1 conversation to MLS 1:1 conversation');
+ await this.eventService.moveEventsToConversation(proteusConversation.id, mlsConversation.id);
const isActiveConversation = this.conversationState.isActiveConversation(proteusConversation);
@@ -1491,18 +1488,18 @@ export class ConversationRepository {
} = proteusConversation;
const updates: Partial> = {
- archivedState,
- archivedTimestamp,
- cleared_timestamp,
- localMessageTimer,
- last_event_timestamp,
- last_read_timestamp,
- last_server_timestamp,
- legalHoldStatus,
- mutedState,
- mutedTimestamp,
- status,
- verification_state,
+ archivedState: archivedState(),
+ archivedTimestamp: archivedTimestamp(),
+ cleared_timestamp: cleared_timestamp(),
+ localMessageTimer: localMessageTimer(),
+ last_event_timestamp: last_event_timestamp(),
+ last_read_timestamp: last_read_timestamp(),
+ last_server_timestamp: last_server_timestamp(),
+ legalHoldStatus: legalHoldStatus(),
+ mutedState: mutedState(),
+ mutedTimestamp: mutedTimestamp(),
+ status: status(),
+ verification_state: verification_state(),
};
ConversationMapper.updateProperties(mlsConversation, updates);
From 96b18333be4ca33a2321d62244dcabd665baeabe Mon Sep 17 00:00:00 2001
From: PatrykBuniX
Date: Mon, 11 Sep 2023 13:30:59 +0200
Subject: [PATCH 50/70] test: move conversation events
---
src/script/event/EventService.ts | 6 +-
test/unit_tests/event/EventServiceCommon.js | 101 ++++++++++++++++++++
2 files changed, 103 insertions(+), 4 deletions(-)
diff --git a/src/script/event/EventService.ts b/src/script/event/EventService.ts
index 3d317488109..9f13df2404d 100644
--- a/src/script/event/EventService.ts
+++ b/src/script/event/EventService.ts
@@ -553,7 +553,7 @@ export class EventService {
*/
async loadAllConversationEvents(
conversationId: string,
- eventTypesToSkip: (CONVERSATION_EVENT | CLIENT_CONVERSATION_EVENT)[],
+ eventTypesToSkip: (CONVERSATION_EVENT | CLIENT_CONVERSATION_EVENT)[] = [],
): Promise {
try {
if (this.storageService.db) {
@@ -567,9 +567,7 @@ export class EventService {
}
const records = await this.storageService.getAll(StorageSchemata.OBJECT_STORE.EVENTS);
- return records.filter(
- record => record.conversation === conversationId && !eventTypesToSkip.includes(record.type),
- );
+ return records;
} catch (error) {
const logMessage = `Failed to get events for conversation '${conversationId}': ${error.message}`;
this.logger.error(logMessage, error);
diff --git a/test/unit_tests/event/EventServiceCommon.js b/test/unit_tests/event/EventServiceCommon.js
index 6baafe12fbf..6a018aec5d3 100644
--- a/test/unit_tests/event/EventServiceCommon.js
+++ b/test/unit_tests/event/EventServiceCommon.js
@@ -507,6 +507,107 @@ const testEventServiceClass = (testedServiceName, className) => {
});
});
+ describe('loadAllConversationEvents', () => {
+ const conversationId = 'conversation-id';
+
+ afterEach(() => {
+ testFactory.storage_service.clearStores();
+ });
+
+ it('Loads all the events by conversation id', async () => {
+ const numberOfConversationEvents = 3;
+ const numberOfOtherConversationEvents = 1;
+
+ const conversationEvents = Array.from({length: numberOfConversationEvents}, () => ({
+ conversation: conversationId,
+ id: createUuid(),
+ time: '2016-08-04T13:27:55.182Z',
+ }));
+
+ const otherConversationEvents = Array.from({length: numberOfOtherConversationEvents}, () => ({
+ conversation: 'other-conversation-id',
+ id: createUuid(),
+ time: '2016-08-04T13:27:55.182Z',
+ }));
+
+ const events = [...conversationEvents, ...otherConversationEvents];
+
+ Promise.all(events.map(event => testFactory.storage_service.save(eventStoreName, undefined, event)));
+
+ const eventService = testFactory[testedServiceName];
+
+ const foundConversationEvents = await eventService.loadAllConversationEvents(conversationId);
+
+ expect(foundConversationEvents.length).toBe(numberOfConversationEvents);
+ });
+
+ it('Skips types of events included in the skip array', async () => {
+ const numberOfConversationEvents = 3;
+ const skipTypes = ['conversation.message-add'];
+ const numberOfEventsToSkip = 2;
+
+ const conversationEvents = Array.from({length: numberOfConversationEvents}, () => ({
+ conversation: conversationId,
+ id: createUuid(),
+ time: '2016-08-04T13:27:55.182Z',
+ }));
+
+ const conversationEventsToSkip = Array.from({length: numberOfEventsToSkip}, () => ({
+ conversation: conversationId,
+ id: createUuid(),
+ time: '2016-08-04T13:27:55.182Z',
+ type: skipTypes[0],
+ }));
+
+ const events = [...conversationEvents, ...conversationEventsToSkip];
+
+ Promise.all(events.map(event => testFactory.storage_service.save(eventStoreName, undefined, event)));
+
+ const eventService = testFactory[testedServiceName];
+
+ const foundConversationEvents = await eventService.loadAllConversationEvents(conversationId, skipTypes);
+
+ expect(foundConversationEvents.length).toBe(numberOfConversationEvents);
+ });
+ });
+
+ describe('moveEventsToConversation', () => {
+ const oldConversationId = 'old-conversation-id';
+ const newConversationId = 'new-conversation-id';
+
+ afterEach(() => {
+ testFactory.storage_service.clearStores();
+ });
+
+ it('Loads all the events by conversation id', async () => {
+ const numberOfConversationEvents = 3;
+
+ const conversationEvents = Array.from({length: numberOfConversationEvents}, () => ({
+ conversation: oldConversationId,
+ id: createUuid(),
+ time: '2016-08-04T13:27:55.182Z',
+ }));
+
+ Promise.all(
+ conversationEvents.map(event => testFactory.storage_service.save(eventStoreName, undefined, event)),
+ );
+
+ const eventService = testFactory[testedServiceName];
+
+ const loadedOldConversationEvents = await eventService.loadAllConversationEvents(oldConversationId);
+ const loadedNewConversationEvents = await eventService.loadAllConversationEvents(newConversationId);
+ expect(loadedOldConversationEvents.length).toBe(numberOfConversationEvents);
+ expect(loadedNewConversationEvents.length).toBe(0);
+
+ await eventService.moveEventsToConversation(oldConversationId, newConversationId);
+
+ const loadedOldConversationEventsAfterMove = await eventService.loadAllConversationEvents(oldConversationId);
+ const loadedNewConversationEventsAfterMove = await eventService.loadAllConversationEvents(newConversationId);
+ expect(loadedOldConversationEventsAfterMove.length).toBe(0);
+ expect(loadedNewConversationEventsAfterMove.length).toBe(numberOfConversationEvents);
+ });
+ });
+
describe('deleteEventByKey', () => {
let primary_keys = undefined;
From 6a3723f8a1257b061a2e3f728456dd6861adb8f7 Mon Sep 17 00:00:00 2001
From: Arjita
Date: Tue, 12 Sep 2023 16:31:39 +0200
Subject: [PATCH 51/70] feat: add readOnly flag for conversation (#15759)
* feat: add readOnly flag for conversation
* fix: fix pipeline issue
* feat: Mark 1:1 conversation as read-only if there's no common protocol
* feat: disable audio/video call buttons if readonly conversation
* refactor: rename variable
* fix: remove test trigger
* fix: code review comments
* fix: test pipeline
* feat: rename variable and trigger conversation readonly
* fix: remove TODO comment
---
src/i18n/en-US.json | 7 +-
.../components/Conversation/Conversation.tsx | 54 +++++++++----
.../ReadOnlyConversationMessage.tsx | 77 +++++++++++++++++++
src/script/components/TitleBar/TitleBar.tsx | 4 +
.../conversation/ConversationFilter.test.ts | 2 +
.../conversation/ConversationRepository.ts | 23 +++++-
src/script/entity/Conversation.ts | 7 +-
src/script/page/AppMain.tsx | 1 +
.../page/MainContent/MainContent.test.tsx | 1 +
src/script/page/MainContent/MainContent.tsx | 3 +
.../storage/record/ConversationRecord.ts | 3 +
src/style/content/conversation.less | 38 +++++++++
test/helper/ConversationGenerator.ts | 1 +
13 files changed, 200 insertions(+), 21 deletions(-)
create mode 100644 src/script/components/Conversation/ReadOnlyConversationMessage.tsx
diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json
index 0f0cc3b57b2..350a21a4eae 100644
--- a/src/i18n/en-US.json
+++ b/src/i18n/en-US.json
@@ -726,9 +726,14 @@
"messageFailedToSendWillNotReceiveSingular": "won't get your message.",
"messageFailedToSendWillReceivePlural": "will get your message later.",
"messageFailedToSendWillReceiveSingular": "will get your message later.",
- "mlsConversationRecovered": "You haven’t used this device for a while, or an issue has occurred. Some older messages may not appear here.",
+ "messageReactionDetails": "{{emojiCount}} reaction, react with {{emojiName}} emoji",
+ "mlsConversationRecovered": "You haven't used this device for a while, or an issue has occurred. Some older messages may not appear here.",
"mlsToggleInfo": "When this is on, conversation will use the new messaging layer security (MLS) protocol.",
"mlsToggleName": "MLS",
+ "selfNotSupportMLSMsgPart1": "You can't communicate with [bold]{{selfUserName}}[/bold] anymore, as your device doesn't support the suitable protocol.",
+ "downloadLatestMLS": "Download the latest MLS Wire version ",
+ "selfNotSupportMLSMsgPart2": " to call, and send messages and files again.",
+ "otherUserNotSupportMLSMsg": "You can't communicate with [bold]{{participantName}}[/bold] anymore, as you two now use different protocols. When [bold]{{participantName}}[/bold] gets an update, you can call and send messages and files again.",
"modalAccountCreateAction": "OK",
"modalAccountCreateHeadline": "Create an account?",
"modalAccountCreateMessage": "By creating an account you will lose the conversation history in this guest room.",
diff --git a/src/script/components/Conversation/Conversation.tsx b/src/script/components/Conversation/Conversation.tsx
index 34ef90da1ce..3832696460e 100644
--- a/src/script/components/Conversation/Conversation.tsx
+++ b/src/script/components/Conversation/Conversation.tsx
@@ -35,6 +35,7 @@ import {showWarningModal} from 'Components/Modals/utils/showWarningModal';
import {TitleBar} from 'Components/TitleBar';
import {CallState} from 'src/script/calling/CallState';
import {Config} from 'src/script/Config';
+import {CONVERSATION_READONLY_STATE} from 'src/script/conversation/ConversationRepository';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {allowsAllFiles, getFileExtensionOrName, hasAllowedExtension} from 'Util/FileTypeUtil';
import {isHittingUploadLimit} from 'Util/isHittingUploadLimit';
@@ -44,6 +45,7 @@ import {safeMailOpen, safeWindowOpen} from 'Util/SanitizationUtil';
import {formatBytes, incomingCssClass, removeAnimationsClass} from 'Util/util';
import {useReadReceiptSender} from './hooks/useReadReceipt';
+import {ReadOnlyConversationMessage} from './ReadOnlyConversationMessage';
import {checkFileSharingPermission} from './utils/checkFileSharingPermission';
import {ConversationState} from '../../conversation/ConversationState';
@@ -71,6 +73,7 @@ interface ConversationProps {
readonly userState: UserState;
openRightSidebar: (panelState: PanelState, params: RightSidebarParams, compareEntityId?: boolean) => void;
isRightSidebarOpen?: boolean;
+ onRefresh: () => void;
}
const CONFIG = Config.getConfig();
@@ -81,6 +84,7 @@ export const Conversation: FC = ({
userState,
openRightSidebar,
isRightSidebarOpen = false,
+ onRefresh,
}) => {
const messageListLogger = getLogger('ConversationList');
@@ -99,7 +103,18 @@ export const Conversation: FC = ({
'classifiedDomains',
'isFileSharingSendingEnabled',
]);
- const {is1to1, isRequest} = useKoSubscribableChildren(activeConversation!, ['is1to1', 'isRequest']);
+ const {
+ is1to1,
+ isRequest,
+ readOnlyState,
+ display_name: displayName,
+ } = useKoSubscribableChildren(activeConversation!, ['is1to1', 'isRequest', 'readOnlyState', 'display_name']);
+ const showReadOnlyConversationMessage =
+ readOnlyState !== null &&
+ [
+ CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS,
+ CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS,
+ ].includes(readOnlyState);
const {self: selfUser} = useKoSubscribableChildren(userState, ['self']);
const {inTeam} = useKoSubscribableChildren(selfUser, ['inTeam']);
@@ -472,6 +487,7 @@ export const Conversation: FC = ({
callActions={mainViewModel.calling.callActions}
openRightSidebar={openRightSidebar}
isRightSidebarOpen={isRightSidebarOpen}
+ isReadOnlyConversation={showReadOnlyConversationMessage}
/>
{activeCalls.map(call => {
@@ -520,22 +536,26 @@ export const Conversation: FC = ({
setMsgElementsFocusable={setMsgElementsFocusable}
/>
- setMsgElementsFocusable(false)}
- uploadDroppedFiles={uploadDroppedFiles}
- uploadImages={uploadImages}
- uploadFiles={uploadFiles}
- />
+ {showReadOnlyConversationMessage ? (
+
+ ) : (
+ setMsgElementsFocusable(false)}
+ uploadDroppedFiles={uploadDroppedFiles}
+ uploadImages={uploadImages}
+ uploadFiles={uploadFiles}
+ />
+ )}
diff --git a/src/script/components/Conversation/ReadOnlyConversationMessage.tsx b/src/script/components/Conversation/ReadOnlyConversationMessage.tsx
new file mode 100644
index 00000000000..0965fcae908
--- /dev/null
+++ b/src/script/components/Conversation/ReadOnlyConversationMessage.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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 {FC} from 'react';
+
+import {Link, LinkVariant} from '@wireapp/react-ui-kit';
+
+import {Icon} from 'Components/Icon';
+import {CONVERSATION_READONLY_STATE} from 'src/script/conversation/ConversationRepository';
+import {t} from 'Util/LocalizerUtil';
+
+interface ReadOnlyConversationMessageProps {
+ state: CONVERSATION_READONLY_STATE;
+ handleMLSUpdate: () => void;
+ displayName: string;
+}
+export const ReadOnlyConversationMessage: FC = ({
+ state,
+ handleMLSUpdate,
+ displayName,
+}) => {
+ const mlsCompatibilityMessage =
+ state === CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS
+ ? t('otherUserNotSupportMLSMsg', displayName)
+ : t('selfNotSupportMLSMsgPart1', displayName);
+
+ return (
+