Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Clean up connections when user joins team (WPB-15263) #18579

Merged
merged 2 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/script/connection/ConnectionRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {StatusCodes} from 'http-status-codes';

import {WebAppEvents} from '@wireapp/webapp-events';

import {SelfService} from 'src/script/self/SelfService';
import {TeamService} from 'src/script/team/TeamService';
import {generateUser} from 'test/helper/UserGenerator';
import {createUuid} from 'Util/uuid';

Expand All @@ -39,9 +41,11 @@ import {UserRepository} from '../user/UserRepository';
function buildConnectionRepository() {
const connectionState = new ConnectionState();
const connectionService = new ConnectionService();
const selfService = new SelfService();
const teamService = new TeamService();
const userRepository = {refreshUser: jest.fn()} as unknown as UserRepository;
return [
new ConnectionRepository(connectionService, userRepository, connectionState),
new ConnectionRepository(connectionService, userRepository, selfService, teamService, connectionState),
{connectionState, userRepository, connectionService},
] as const;
}
Expand Down Expand Up @@ -130,7 +134,7 @@ describe('ConnectionRepository', () => {

jest.spyOn(connectionService, 'getConnections').mockResolvedValue([connectionRequest, connectionRequest]);

await connectionRepository.getConnections();
await connectionRepository.getConnections([]);

const storedConnection = connectionRepository.getConnectionByConversationId({
id: connectionRequest.conversation,
Expand Down
114 changes: 91 additions & 23 deletions src/script/connection/ConnectionRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@
*/

import {ConnectionStatus} from '@wireapp/api-client/lib/connection/';
import {UserConnectionEvent, USER_EVENT} from '@wireapp/api-client/lib/event/';
import type {BackendEventType} from '@wireapp/api-client/lib/event/BackendEvent';
import {UserConnectionEvent, USER_EVENT, UserEvent} from '@wireapp/api-client/lib/event/';
import {BackendErrorLabel} from '@wireapp/api-client/lib/http/';
import {QualifiedId} from '@wireapp/api-client/lib/user';
import type {UserConnectionData} from '@wireapp/api-client/lib/user/data/';
import type {UserConnectionData, UserUpdateData} from '@wireapp/api-client/lib/user/data/';
import {amplify} from 'amplify';
import {container} from 'tsyringe';

import {WebAppEvents} from '@wireapp/webapp-events';

import {SelfService} from 'src/script/self/SelfService';
import {TeamService} from 'src/script/team/TeamService';
import {UserState} from 'src/script/user/UserState';
import {replaceLink, t} from 'Util/LocalizerUtil';
import {getLogger, Logger} from 'Util/Logger';
import {matchQualifiedIds} from 'Util/QualifiedId';
Expand All @@ -54,16 +56,13 @@ export class ConnectionRepository {
private readonly logger: Logger;
private onDeleteConnectionRequestConversation?: (userId: QualifiedId) => Promise<void>;

static get CONFIG(): Record<string, BackendEventType[]> {
return {
SUPPORTED_EVENTS: [USER_EVENT.CONNECTION],
};
}

constructor(
connectionService: ConnectionService,
userRepository: UserRepository,
private readonly selfService: SelfService,
private readonly teamService: TeamService,
private readonly connectionState = container.resolve(ConnectionState),
private readonly userState = container.resolve(UserState),
) {
this.connectionService = connectionService;
this.userRepository = userRepository;
Expand All @@ -79,20 +78,27 @@ export class ConnectionRepository {
* @param eventJson JSON data for event
* @param source Source of event
*/
private readonly onUserEvent = async (eventJson: UserConnectionEvent, source: EventSource) => {
private readonly onUserEvent = async (eventJson: UserEvent, source: EventSource) => {
const eventType = eventJson.type;

const isSupportedType = ConnectionRepository.CONFIG.SUPPORTED_EVENTS.includes(eventType);
if (isSupportedType) {
this.logger.info(`User Event: '${eventType}' (Source: ${source})`);

const isUserConnection = eventType === USER_EVENT.CONNECTION;
if (isUserConnection) {
await this.onUserConnection(eventJson, source);
}
switch (eventType) {
case USER_EVENT.CONNECTION:
await this.onUserConnection(eventJson as UserConnectionEvent, source);
break;
case USER_EVENT.UPDATE:
await this.onUserUpdate(eventJson);
break;
}
};

private async onUserUpdate(eventJson: UserUpdateData) {
if (eventJson.user.id === this.userState.self()?.qualifiedId.id) {
await this.deletePendingConnectionsToSelfNewTeamMembers();
return;
}
await this.deletePendingConnectionToNewTeamMember(eventJson);
}

/**
* Convert a JSON event into an entity and get the matching conversation.
*
Expand Down Expand Up @@ -269,9 +275,17 @@ export class ConnectionRepository {
*
* @returns Promise that resolves when all connections have been retrieved and mapped
*/
async getConnections(): Promise<ConnectionEntity[]> {
async getConnections(teamMembers: QualifiedId[]): Promise<ConnectionEntity[]> {
const connectionData = await this.connectionService.getConnections();
const connections = ConnectionMapper.mapConnectionsFromJson(connectionData);

const acceptedConnectionsOrNoneTeamMembersConnections = connectionData.filter(connection => {
const isTeamMember = teamMembers.some(teamMemberQualifiedId =>
matchQualifiedIds(connection.qualified_to, teamMemberQualifiedId),
);
return !isTeamMember || connection.status === ConnectionStatus.ACCEPTED;
});

const connections = ConnectionMapper.mapConnectionsFromJson(acceptedConnectionsOrNoneTeamMembersConnections);

this.connectionState.connections(connections);
return connections;
Expand Down Expand Up @@ -375,17 +389,17 @@ export class ConnectionRepository {
}
}

private async deleteConnectionWithUser(user: User) {
public async deleteConnectionWithUser(user: User) {
const connection = this.connectionState
.connections()
.find(connection => matchQualifiedIds(connection.userId, user.qualifiedId));

await this.onDeleteConnectionRequestConversation?.(user.qualifiedId);

if (connection) {
this.connectionState.connections.remove(connection);
user.connection(null);
}

await this.onDeleteConnectionRequestConversation?.(user.qualifiedId);
}

/**
Expand Down Expand Up @@ -437,4 +451,58 @@ export class ConnectionRepository {
amplify.publish(WebAppEvents.NOTIFICATION.NOTIFY, messageEntity, connectionEntity);
}
}

async deletePendingConnectionsToSelfNewTeamMembers() {
const freshSelf = await this.selfService.getSelf([]);
const newTeamId = freshSelf.team;

if (!newTeamId) {
return;
}

const currentConnectionsUserIds = this.connectionState.connections().map(connection => connection.userId);
const currentConnectionsUsers = await this.userRepository.getUsersById(currentConnectionsUserIds);

const teamMembersToDeletePendingConnectionsWith = await this.teamService.getTeamMembersByIds(
newTeamId,
currentConnectionsUsers.map(user => user.qualifiedId.id),
);

const currentUsersToDeleteConnectionWith = currentConnectionsUsers.filter(user => {
return teamMembersToDeletePendingConnectionsWith.some(member => member.user === user.qualifiedId.id);
});

for (const user of currentUsersToDeleteConnectionWith) {
await this.deleteConnectionWithUser(user);
}
}

async deletePendingConnectionToNewTeamMember(event: UserUpdateData) {
const newlyJoinedUserId = event.user.id;
const selfUserDomain = this.userState.self()?.domain;
const newlyJoinedUserQualifiedId = {
id: newlyJoinedUserId,
/*
we can assume that the domain of the user is the same as the self user domain
because they have joined our team
*/
domain: selfUserDomain ?? '',
};

const newlyJoinedUser = await this.userRepository.getUserById(newlyJoinedUserQualifiedId);
const connectionWithNewlyJoinedUser = newlyJoinedUser.connection();
const conversationIdWithNewlyJoinedUser = connectionWithNewlyJoinedUser?.conversationId;

// If the connection is already accepted, we don't need to delete the conversation from our state
// we're gonna use the previous 1:1 conversation with the newly joined user
if (
!connectionWithNewlyJoinedUser ||
!conversationIdWithNewlyJoinedUser ||
connectionWithNewlyJoinedUser?.status() === ConnectionStatus.ACCEPTED
) {
return;
}

await this.deleteConnectionWithUser(newlyJoinedUser);
}
}
6 changes: 5 additions & 1 deletion src/script/conversation/ConversationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,11 @@ export class ConversationState {
/**
* Check whether conversation is currently displayed.
*/
isActiveConversation(conversationEntity: Conversation): boolean {
isActiveConversation(conversationEntity?: Conversation): boolean {
if (!conversationEntity) {
return false;
}

const activeConversation = this.activeConversation();
return !!activeConversation && !!conversationEntity && matchQualifiedIds(activeConversation, conversationEntity);
}
Expand Down
19 changes: 15 additions & 4 deletions src/script/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import {APIClient} from '../service/APIClientSingleton';
import {Core} from '../service/CoreSingleton';
import {StorageKey, StorageRepository, StorageService} from '../storage';
import {TeamRepository} from '../team/TeamRepository';
import {TeamService} from '../team/TeamService';
import {AppInitStatisticsValue} from '../telemetry/app_init/AppInitStatisticsValue';
import {AppInitTelemetry} from '../telemetry/app_init/AppInitTelemetry';
import {AppInitTimingsStep} from '../telemetry/app_init/AppInitTimingsStep';
Expand Down Expand Up @@ -207,6 +208,7 @@ export class App {
private _setupRepositories() {
const repositories: ViewModelRepositories = {} as ViewModelRepositories;
const selfService = new SelfService();
const teamService = new TeamService();

repositories.asset = container.resolve(AssetRepository);

Expand All @@ -228,11 +230,20 @@ export class App {
serverTimeHandler,
repositories.properties,
);
repositories.connection = new ConnectionRepository(new ConnectionService(), repositories.user);
repositories.connection = new ConnectionRepository(
new ConnectionService(),
repositories.user,
selfService,
teamService,
);
repositories.event = new EventRepository(this.service.event, this.service.notification, serverTimeHandler);
repositories.search = new SearchRepository(repositories.user);
repositories.team = new TeamRepository(repositories.user, repositories.asset, () =>
this.logout(SIGN_OUT_REASON.ACCOUNT_DELETED, true),

repositories.team = new TeamRepository(
repositories.user,
repositories.asset,
() => this.logout(SIGN_OUT_REASON.ACCOUNT_DELETED, true),
teamService,
);

repositories.message = new MessageRepository(
Expand Down Expand Up @@ -455,7 +466,7 @@ export class App {
onProgress(10);
telemetry.timeStep(AppInitTimingsStep.INITIALIZED_CRYPTOGRAPHY);

const connections = await connectionRepository.getConnections();
const connections = await connectionRepository.getConnections(teamMembers);

telemetry.timeStep(AppInitTimingsStep.RECEIVED_USER_DATA);

Expand Down
11 changes: 9 additions & 2 deletions test/helper/TestFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,12 @@ export class TestFactory {
await this.exposeUserActors();
this.connection_service = new ConnectionService();

this.connection_repository = new ConnectionRepository(this.connection_service, this.user_repository);
this.connection_repository = new ConnectionRepository(
this.connection_service,
this.user_repository,
this.self_service,
this.team_service,
);

return this.connection_repository;
}
Expand Down Expand Up @@ -223,8 +228,10 @@ export class TestFactory {
await this.exposeTeamActors();
await this.exposeClientActors();

this.self_service = new SelfService();

this.self_repository = new SelfRepository(
new SelfService(),
this.self_service,
this.user_repository,
this.team_repository,
this.client_repository,
Expand Down
Loading