Skip to content

Commit

Permalink
feat: user joins mls group with guest link (#14832)
Browse files Browse the repository at this point in the history
* feat: add other self clients to mls group after user has joined via link

* refactor: create separate module for maintaining link joined conversation id

* refactor: improve finalization api

* refactor: include code join flow finalization in onmemberjoin handler

* runfix: wrapp adding self clients with try catch

* test: add test case

* runfix: dont try to add self clients if mls conversation is not established

* refactor: organise if statements

* refactor: address cr comments

* chore: remove irrelevant comment
  • Loading branch information
PatrykBuniX committed Mar 20, 2023
1 parent 9c7cef5 commit 55d419a
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 15 deletions.
1 change: 1 addition & 0 deletions src/__mocks__/@wireapp/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class Account extends EventEmitter {
conversation: {
send: jest.fn(),
isMLSConversationEstablished: jest.fn(),
addUsersToMLSConversation: jest.fn(),
messageTimer: {
setConversationLevelTimer: jest.fn(),
},
Expand Down
48 changes: 47 additions & 1 deletion src/script/conversation/ConversationRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,21 @@ jest.deepUnmock('axios');
const _generateConversation = (
conversation_type = CONVERSATION_TYPE.REGULAR,
connection_status = ConnectionStatus.ACCEPTED,
conversationProtocol = ConversationProtocol.PROTEUS,
domain = '',
) => {
const conversation = new Conversation(createRandomUuid(), '');
const conversation = new Conversation(createRandomUuid(), domain, conversationProtocol);
conversation.type(conversation_type);

const connectionEntity = new ConnectionEntity();
connectionEntity.conversationId = conversation.qualifiedId;
connectionEntity.status(connection_status);
conversation.connection(connectionEntity);

if (conversationProtocol === ConversationProtocol.MLS) {
conversation.groupId = 'groupId';
}

return conversation;
};

Expand Down Expand Up @@ -746,6 +752,46 @@ describe('ConversationRepository', () => {
});
});

it('should add other self clients to mls group if user was event creator', () => {
const mockDomain = 'example.com';
const mockSelfClientId = 'self-client-id';
const selfUser = UserGenerator.getRandomUser(mockDomain);

const conversationEntity = _generateConversation(
CONVERSATION_TYPE.REGULAR,
undefined,
ConversationProtocol.MLS,
mockDomain,
);
testFactory.conversation_repository['saveConversation'](conversationEntity);

const memberJoinEvent = {
conversation: conversationEntity.id,
data: {
user_ids: [selfUser.id],
},
from: selfUser.id,
time: '2015-04-27T11:42:31.475Z',
type: CONVERSATION_EVENT.MEMBER_JOIN,
} as ConversationMemberJoinEvent;

spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser);

Object.defineProperty(container.resolve(Core), 'clientId', {
get: jest.fn(() => mockSelfClientId),
});

return testFactory.conversation_repository['handleConversationEvent'](memberJoinEvent).then(() => {
expect(testFactory.conversation_repository['onMemberJoin']).toHaveBeenCalled();
expect(testFactory.conversation_repository.updateParticipatingUserEntities).toHaveBeenCalled();
expect(container.resolve(Core).service!.conversation.addUsersToMLSConversation).toHaveBeenCalledWith({
conversationId: conversationEntity.qualifiedId,
groupId: 'groupId',
qualifiedUsers: [{domain: mockDomain, id: selfUser.id, skipOwnClientId: mockSelfClientId}],
});
});
});

it('should ignore member-join event when joining a 1to1 conversation', () => {
const selfUser = UserGenerator.getRandomUser();
const conversationRepo = testFactory.conversation_repository!;
Expand Down
71 changes: 59 additions & 12 deletions src/script/conversation/ConversationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ import * as LegalHoldEvaluator from '../legal-hold/LegalHoldEvaluator';
import {MessageCategory} from '../message/MessageCategory';
import {SuperType} from '../message/SuperType';
import {SystemMessageType} from '../message/SystemMessageType';
import {useMLSConversationState} from '../mls';
import {addOtherSelfClientsToMLSConversation, useMLSConversationState} from '../mls';
import {PropertiesRepository} from '../properties/PropertiesRepository';
import {Core} from '../service/CoreSingleton';
import type {EventRecord} from '../storage';
Expand Down Expand Up @@ -2460,28 +2460,32 @@ export class ConversationRepository {
});
}

// Self user joins again
const selfUserRejoins = eventData.user_ids.includes(this.userState.self().id);
if (selfUserRejoins) {
// Self user is a creator of the event
const isFromSelf = eventJson.from === this.userState.self().id;

const containsSelfId = eventData.user_ids.includes(this.userState.self().id);
const containsSelfQualifiedId = !!eventData.users?.some(
({qualified_id: qualifiedId}) => qualifiedId && matchQualifiedIds(qualifiedId, this.userState.self().qualifiedId),
);

const selfUserJoins = containsSelfId || containsSelfQualifiedId;

if (selfUserJoins) {
conversationEntity.status(ConversationStatus.CURRENT_MEMBER);
await this.conversationRoleRepository.updateConversationRoles(conversationEntity);
}

const updateSequence =
selfUserRejoins || connectionEntity?.isConnected()
selfUserJoins || connectionEntity?.isConnected()
? this.updateConversationFromBackend(conversationEntity)
: Promise.resolve();

const qualifiedUserIds =
eventData.users?.map(user => user.qualified_id) || eventData.user_ids.map(userId => ({domain: '', id: userId}));

if (
conversationEntity.groupId &&
!useMLSConversationState.getState().isEstablished(conversationEntity.groupId) &&
(await this.core.service!.conversation.isMLSConversationEstablished(conversationEntity.groupId))
) {
// If the conversation was not previously marked as established and the core if aware of this conversation, we can mark is as established
useMLSConversationState.getState().markAsEstablished(conversationEntity.groupId);
if (conversationEntity.isUsingMLSProtocol) {
const isSelfJoin = isFromSelf && selfUserJoins;
await this.handleMLSConversationMemberJoin(conversationEntity, isSelfJoin);
}

return updateSequence
Expand All @@ -2493,6 +2497,49 @@ export class ConversationRepository {
});
}

/**
* Handles member join event on mls group - updating mls conversation state and adding other self clients if user has joined by itself.
*
* @param conversation Conversation member joined to
* @param isSelfJoin whether user has joined by itself, if so we need to add other self clients to mls group
*/
private async handleMLSConversationMemberJoin(conversation: Conversation, isSelfJoin: boolean) {
const {groupId} = conversation;

if (!groupId) {
throw new Error(`groupId not found for MLS conversation ${conversation.id}`);
}

const isMLSConversationEstablished = await this.core.service!.conversation.isMLSConversationEstablished(groupId);

if (!isMLSConversationEstablished) {
return;
}

const mlsConversationState = useMLSConversationState.getState();

const isMLSConversationMarkedAsEstablished = mlsConversationState.isEstablished(groupId);

if (!isMLSConversationMarkedAsEstablished) {
// If the conversation was not previously marked as established and the core if aware of this conversation, we can mark is as established
mlsConversationState.markAsEstablished(groupId);
}

if (isSelfJoin) {
// if user has joined and was also event creator (eg. joined via guest link) we need to add its other clients to mls group
try {
await addOtherSelfClientsToMLSConversation(
conversation,
this.userState.self().qualifiedId,
this.core.clientId,
this.core,
);
} catch (error) {
this.logger.warn(`Failed to add other self clients to MLS conversation: ${conversation.id}`, error);
}
}
}

/**
* Members of a group conversation were removed or left.
*
Expand Down
35 changes: 35 additions & 0 deletions src/script/mls/MLSConversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
*
*/

import {QualifiedId} from '@wireapp/api-client/lib/user';
import {KeyPackageClaimUser} from '@wireapp/core/lib/conversation';

import {Account} from '@wireapp/core';

import {useMLSConversationState} from './mlsConversationState';
Expand Down Expand Up @@ -100,3 +103,35 @@ export async function registerUninitializedConversations(
),
);
}

/**
* Will add all other user's self clients to the mls group.
*
* @param conversation id of the conversation
* @param selfUserId id of the self user who's clients should be added
* @param selfClientId id of the current client (that should be skipped)
* @param core instance of the core
*/
export async function addOtherSelfClientsToMLSConversation(
conversation: Conversation,
selfUserId: QualifiedId,
selfClientId: string,
core: Account,
) {
const {groupId, qualifiedId} = conversation;

if (!groupId) {
throw new Error(`No group id found for MLS conversation ${conversation.id}`);
}

const selfQualifiedUser: KeyPackageClaimUser = {
...selfUserId,
skipOwnClientId: selfClientId,
};

await core.service?.conversation.addUsersToMLSConversation({
conversationId: qualifiedId,
groupId,
qualifiedUsers: [selfQualifiedUser],
});
}
7 changes: 5 additions & 2 deletions test/helper/UserGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import {serverTimeHandler} from '../../src/script/time/serverTimeHandler';
import {UserMapper} from '../../src/script/user/UserMapper';

export class UserGenerator {
static getRandomUser(): User {
static getRandomUser(domain?: string): User {
const id = createRandomUuid();

const template: APIClientUser = {
accent_id: Math.floor(Math.random() * 7 + 1),
assets: [
Expand All @@ -44,8 +46,9 @@ export class UserGenerator {
},
],
handle: faker.internet.userName(),
id: createRandomUuid(),
id,
name: faker.name.fullName(),
qualified_id: domain ? {id, domain} : undefined,
};

return new UserMapper(serverTimeHandler).mapUserFromJson(template);
Expand Down

0 comments on commit 55d419a

Please sign in to comment.