Skip to content

Commit 30a7636

Browse files
committed
Implement accept and decline match
1 parent 94f005d commit 30a7636

File tree

11 files changed

+230
-38
lines changed

11 files changed

+230
-38
lines changed

backend/gateway-service/package-lock.json

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/gateway-service/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"ioredis": "^5.4.1",
3535
"passport-jwt": "^4.0.1",
3636
"reflect-metadata": "^0.1.13",
37-
"rxjs": "^7.8.1"
37+
"rxjs": "^7.8.1",
38+
"uuid": "^10.0.0"
3839
},
3940
"devDependencies": {
4041
"@nestjs/cli": "^10.0.0",
@@ -44,6 +45,7 @@
4445
"@types/jest": "^29.5.2",
4546
"@types/node": "^20.3.1",
4647
"@types/supertest": "^2.0.12",
48+
"@types/uuid": "^10.0.0",
4749
"@typescript-eslint/eslint-plugin": "^6.0.0",
4850
"@typescript-eslint/parser": "^6.0.0",
4951
"eslint": "^8.42.0",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { UserMatchOptionsDto } from './user-match-options.dto';
22
export { MatchRequestDto } from './match-request.dto';
3+
export { MatchAcceptDto } from './match-accept.dto';
4+
export { MatchDeclineDto } from './match-decline.dto';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { IsNotEmpty, IsString } from 'class-validator';
2+
3+
export class MatchAcceptDto {
4+
@IsString()
5+
@IsNotEmpty()
6+
userId: string;
7+
8+
@IsString()
9+
@IsNotEmpty()
10+
matchId: string;
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { IsNotEmpty, IsString } from 'class-validator';
2+
3+
export class MatchDeclineDto {
4+
@IsString()
5+
@IsNotEmpty()
6+
userId: string;
7+
8+
@IsString()
9+
@IsNotEmpty()
10+
matchId: string;
11+
}

backend/gateway-service/src/modules/match/match.controller.ts

Lines changed: 152 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,25 @@ import { ClientProxy } from '@nestjs/microservices';
1111
import { Inject } from '@nestjs/common';
1212
import { firstValueFrom } from 'rxjs';
1313
import { RedisService } from './redis.service';
14-
import { MatchRequestDto } from './dto';
14+
import { v4 as uuidv4 } from 'uuid';
15+
import { MatchAcceptDto, MatchDeclineDto, MatchRequestDto } from './dto';
1516
import {
1617
MATCH_FOUND,
1718
MATCH_CANCELLED,
1819
MATCH_CONFIRMED,
1920
MATCH_TIMEOUT,
2021
MATCH_REQUESTED,
22+
MATCH_ACCEPTED,
2123
MATCH_ERROR,
2224
EXCEPTION,
25+
MATCH_DECLINED,
2326
} from './match.event';
24-
import { CANCEL_MATCH, FIND_MATCH } from './match.message';
27+
import {
28+
ACCEPT_MATCH,
29+
CANCEL_MATCH,
30+
DECLINE_MATCH,
31+
FIND_MATCH,
32+
} from './match.message';
2533

2634
@WebSocketGateway({
2735
namespace: '/match',
@@ -35,6 +43,8 @@ import { CANCEL_MATCH, FIND_MATCH } from './match.message';
3543
export class MatchGateway implements OnGatewayInit {
3644
@WebSocketServer() server: Server;
3745
private userSockets: Map<string, string> = new Map();
46+
private matchConfirmations: Map<string, Set<string>> = new Map();
47+
private matchParticipants: Map<string, Set<string>> = new Map();
3848

3949
constructor(
4050
@Inject('MATCHING_SERVICE') private matchingClient: ClientProxy,
@@ -63,7 +73,7 @@ export class MatchGateway implements OnGatewayInit {
6373
!payload.selectedTopic ||
6474
!payload.selectedDifficulty
6575
) {
66-
client.emit(EXCEPTION, 'Invalid match request payload.');
76+
client.emit(MATCH_ERROR, 'Invalid match request payload.');
6777
return;
6878
}
6979

@@ -81,7 +91,7 @@ export class MatchGateway implements OnGatewayInit {
8191
message: result.message,
8292
});
8393
} else {
84-
client.emit(EXCEPTION, result.message);
94+
client.emit(MATCH_ERROR, result.message);
8595
}
8696
} catch (error) {
8797
client.emit(EXCEPTION, `Error requesting match: ${error.message}`);
@@ -95,10 +105,7 @@ export class MatchGateway implements OnGatewayInit {
95105
@MessageBody() payload: { userId: string },
96106
) {
97107
if (!payload.userId) {
98-
client.emit(
99-
EXCEPTION,
100-
'Invalid userId. Please check your payload and try again.',
101-
);
108+
client.emit(MATCH_ERROR, 'Invalid userId in payload.');
102109
return;
103110
}
104111

@@ -116,7 +123,7 @@ export class MatchGateway implements OnGatewayInit {
116123
message: result.message,
117124
});
118125
} else {
119-
client.emit(EXCEPTION, result.message);
126+
client.emit(MATCH_ERROR, result.message);
120127
}
121128
} catch (error) {
122129
console.log(error);
@@ -125,23 +132,141 @@ export class MatchGateway implements OnGatewayInit {
125132
}
126133
}
127134

128-
// Notify both matched users via WebSocket
135+
@SubscribeMessage(ACCEPT_MATCH)
136+
async handleAcceptMatch(
137+
@ConnectedSocket() client: Socket,
138+
@MessageBody() payload: MatchAcceptDto,
139+
) {
140+
const { userId, matchId } = payload;
141+
142+
if (!userId || !matchId) {
143+
client.emit(MATCH_ERROR, 'Invalid payload.');
144+
return;
145+
}
146+
147+
if (!this.validateUserId(client, userId)) {
148+
return;
149+
}
150+
151+
// Validate if the matchId exists and check if the user is a valid participant
152+
const participants = this.matchParticipants.get(matchId);
153+
if (!participants || !participants.has(userId)) {
154+
client.emit(MATCH_ERROR, 'You are not a participant of this match.');
155+
return;
156+
}
157+
158+
// Check if the user has already accepted the match
159+
const confirmations = this.matchConfirmations.get(matchId) || new Set();
160+
if (confirmations.has(userId)) {
161+
client.emit(MATCH_ERROR, 'You have already accepted this match.');
162+
return;
163+
}
164+
165+
// Add user's confirmation for the match
166+
confirmations.add(userId);
167+
this.matchConfirmations.set(matchId, confirmations);
168+
169+
// Check if both participants have confirmed the match
170+
if (confirmations.size === 2) {
171+
this.notifyUsersMatchConfirmed(matchId, [...confirmations]);
172+
} else {
173+
client.emit(MATCH_ACCEPTED, {
174+
message: 'Waiting for the other user to accept the match.',
175+
});
176+
}
177+
}
178+
179+
@SubscribeMessage(DECLINE_MATCH)
180+
async handleDeclineMatch(
181+
@ConnectedSocket() client: Socket,
182+
@MessageBody() payload: MatchDeclineDto,
183+
) {
184+
const { userId, matchId } = payload;
185+
186+
if (!userId || !matchId) {
187+
client.emit(MATCH_ERROR, 'Invalid payload.');
188+
return;
189+
}
190+
191+
if (!this.validateUserId(client, userId)) {
192+
return;
193+
}
194+
195+
// Validate if the matchId exists and check if the user is a valid participant
196+
const participants = this.matchParticipants.get(matchId);
197+
if (!participants || !participants.has(userId)) {
198+
client.emit(MATCH_ERROR, 'You are not a participant of this match.');
199+
return;
200+
}
201+
202+
// Notify the other user that the match has been declined
203+
this.notifyOtherUserMatchDeclined(matchId, userId);
204+
205+
// Remove match-related data
206+
this.matchParticipants.delete(matchId);
207+
this.matchConfirmations.delete(matchId);
208+
client.emit(MATCH_DECLINED, { message: 'You have declined the match.' });
209+
}
210+
211+
// Notify both users when they are matched
129212
notifyUsersWithMatch(matchedUsers: string[]) {
130213
const [user1, user2] = matchedUsers;
131214
const user1SocketId = this.getUserSocketId(user1);
132215
const user2SocketId = this.getUserSocketId(user2);
216+
133217
if (user1SocketId && user2SocketId) {
218+
const matchId = this.generateMatchId();
134219
this.server.to(user1SocketId).emit(MATCH_FOUND, {
135-
message: `You have been matched with user ${user2}`,
136-
matchedUserId: user2,
220+
message: `You have found a match`,
221+
matchUserId: user2,
222+
matchId,
137223
});
138224
this.server.to(user2SocketId).emit(MATCH_FOUND, {
139-
message: `You have been matched with user ${user1}`,
140-
matchedUserId: user1,
225+
message: `You have found a match`,
226+
matchUserId: user1,
227+
matchId,
141228
});
229+
230+
// Store participants for this matchId
231+
this.matchParticipants.set(matchId, new Set([user1, user2]));
142232
}
143233
}
144234

235+
// Notify both users when they both accept the match
236+
notifyUsersMatchConfirmed(matchId: string, users: string[]) {
237+
const sessionId = this.generateSessionId();
238+
users.forEach((user) => {
239+
const socketId = this.getUserSocketId(user);
240+
if (socketId) {
241+
this.server.to(socketId).emit(MATCH_CONFIRMED, {
242+
message: `Match confirmed! New session created.`,
243+
sessionId,
244+
});
245+
}
246+
});
247+
248+
// Clean up match participants and confirmations
249+
this.matchConfirmations.delete(matchId);
250+
this.matchParticipants.delete(matchId);
251+
}
252+
253+
private notifyOtherUserMatchDeclined(
254+
matchId: string,
255+
decliningUserId: string,
256+
) {
257+
const participants = this.matchParticipants.get(matchId);
258+
participants?.forEach((participantId) => {
259+
if (participantId !== decliningUserId) {
260+
const socketId = this.getUserSocketId(participantId);
261+
if (socketId) {
262+
this.server.to(socketId).emit(MATCH_DECLINED, {
263+
message: 'The other user has declined the match.',
264+
});
265+
}
266+
}
267+
});
268+
}
269+
145270
notifyUsersWithTimeout(timedOutUsers: string[]) {
146271
timedOutUsers.forEach((user) => {
147272
const socketId = this.getUserSocketId(user);
@@ -213,19 +338,17 @@ export class MatchGateway implements OnGatewayInit {
213338
);
214339

215340
if (result.success) {
216-
console.log(`Match canceled successfully for user ${userId}`);
341+
console.log(`Match auto cancelled user ${userId} at disconnect`);
217342
} else {
218-
console.warn(
219-
`Match cancellation failed for user ${userId}: ${result.message}`,
220-
);
343+
console.log(`No match cancelled: ${result.message}`);
221344
}
222345

223346
this.userSockets.delete(userId);
224347
console.log(`User ${userId} disconnected and removed from userSockets.`);
225348
} catch (error) {
226349
client.emit(
227350
EXCEPTION,
228-
`Error canceling match for user ${userId}: ${error.message}`,
351+
`Error disconnecting user ${userId}: ${error.message}`,
229352
);
230353
}
231354
}
@@ -234,8 +357,17 @@ export class MatchGateway implements OnGatewayInit {
234357
return this.userSockets.get(userId);
235358
}
236359

360+
private generateMatchId(): string {
361+
return uuidv4();
362+
}
363+
364+
// TODO - to be replaced with colab method in the future
365+
private generateSessionId(): string {
366+
return uuidv4();
367+
}
368+
237369
private emitExceptionAndDisconnect(client: Socket, message: string) {
238-
client.emit(EXCEPTION, `Error connecting to /match socket: ${message}`);
370+
client.emit(EXCEPTION, `Error: ${message}`);
239371
client.disconnect();
240372
}
241373

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
export const MATCH_FOUND = 'matchFound'; // Emitted when a match is found
2-
export const MATCH_REQUESTED = 'matchRequested'; // Emitted when a user requests a match
3-
export const MATCH_CANCELLED = 'matchCancelled'; // Emitted when a match is cancelled
4-
export const MATCH_CONFIRMED = 'matchConfirmed'; // Emitted when a match is confirmed
1+
export const MATCH_FOUND = 'matchFound';
2+
export const MATCH_REQUESTED = 'matchRequested';
3+
export const MATCH_CANCELLED = 'matchCancelled';
4+
export const MATCH_ACCEPTED = 'matchAccepted';
5+
export const MATCH_DECLINED = 'matchDeclined';
6+
export const MATCH_CONFIRMED = 'matchConfirmed';
57
export const MATCH_TIMEOUT = 'matchTimeout'; // Emitted after 5 minutes of inactivity
6-
export const MATCH_ERROR = 'matchError'; // Emitted when an error occurs
7-
export const EXCEPTION = 'exception'; // Emitted when an exception occurs
8+
export const MATCH_ERROR = 'matchError';
9+
export const EXCEPTION = 'exception';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export const FIND_MATCH = 'findMatch';
22
export const CANCEL_MATCH = 'cancelMatch';
3+
export const ACCEPT_MATCH = 'acceptMatch';
4+
export const DECLINE_MATCH = 'declineMatch';

0 commit comments

Comments
 (0)