Skip to content

Commit

Permalink
fix: Clean up connections when user joins team (WPB-15263)
Browse files Browse the repository at this point in the history
  • Loading branch information
thisisamir98 committed Jan 13, 2025
1 parent 23f1db1 commit d9bd4bd
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 30 deletions.
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

0 comments on commit d9bd4bd

Please sign in to comment.