friends cursor broadcast system (?)
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { FriendsController } from './friends.controller';
|
import { FriendsController } from './friends.controller';
|
||||||
import { FriendsService } from './friends.service';
|
import { FriendsService } from './friends.service';
|
||||||
import { DatabaseModule } from '../database/database.module';
|
import { DatabaseModule } from '../database/database.module';
|
||||||
@@ -7,7 +7,12 @@ import { UsersModule } from '../users/users.module';
|
|||||||
import { WsModule } from '../ws/ws.module';
|
import { WsModule } from '../ws/ws.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DatabaseModule, AuthModule, UsersModule, WsModule],
|
imports: [
|
||||||
|
DatabaseModule,
|
||||||
|
AuthModule,
|
||||||
|
UsersModule,
|
||||||
|
forwardRef(() => WsModule),
|
||||||
|
],
|
||||||
controllers: [FriendsController],
|
controllers: [FriendsController],
|
||||||
providers: [FriendsService],
|
providers: [FriendsService],
|
||||||
exports: [FriendsService],
|
exports: [FriendsService],
|
||||||
|
|||||||
5
src/types/socket.d.ts
vendored
5
src/types/socket.d.ts
vendored
@@ -6,5 +6,8 @@ export type AuthenticatedSocket = BaseSocket<
|
|||||||
DefaultEventsMap, // ClientToServerEvents
|
DefaultEventsMap, // ClientToServerEvents
|
||||||
DefaultEventsMap, // ServerToClientEvents
|
DefaultEventsMap, // ServerToClientEvents
|
||||||
DefaultEventsMap, // InterServerEvents
|
DefaultEventsMap, // InterServerEvents
|
||||||
{ user?: AuthenticatedUser }
|
{
|
||||||
|
user?: AuthenticatedUser;
|
||||||
|
friends?: Set<string>; // Set of friend user IDs
|
||||||
|
}
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger, Inject, forwardRef } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
OnGatewayConnection,
|
OnGatewayConnection,
|
||||||
OnGatewayDisconnect,
|
OnGatewayDisconnect,
|
||||||
@@ -14,7 +14,10 @@ import type { AuthenticatedSocket } from '../../types/socket';
|
|||||||
import { AuthService } from '../../auth/auth.service';
|
import { AuthService } from '../../auth/auth.service';
|
||||||
import { JwtVerificationService } from '../../auth/services/jwt-verification.service';
|
import { JwtVerificationService } from '../../auth/services/jwt-verification.service';
|
||||||
import { CursorPositionDto } from '../dto/cursor-position.dto';
|
import { CursorPositionDto } from '../dto/cursor-position.dto';
|
||||||
import { FriendRequestWithRelations } from '../../friends/friends.service';
|
import {
|
||||||
|
FriendRequestWithRelations,
|
||||||
|
FriendsService,
|
||||||
|
} from '../../friends/friends.service';
|
||||||
|
|
||||||
const WS_EVENT = {
|
const WS_EVENT = {
|
||||||
CURSOR_REPORT_POSITION: 'cursor-report-position',
|
CURSOR_REPORT_POSITION: 'cursor-report-position',
|
||||||
@@ -22,6 +25,7 @@ const WS_EVENT = {
|
|||||||
FRIEND_REQUEST_ACCEPTED: 'friend-request-accepted',
|
FRIEND_REQUEST_ACCEPTED: 'friend-request-accepted',
|
||||||
FRIEND_REQUEST_DENIED: 'friend-request-denied',
|
FRIEND_REQUEST_DENIED: 'friend-request-denied',
|
||||||
UNFRIENDED: 'unfriended',
|
UNFRIENDED: 'unfriended',
|
||||||
|
FRIEND_CURSOR_POSITION: 'friend-cursor-position',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
@@ -41,6 +45,8 @@ export class StateGateway
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly jwtVerificationService: JwtVerificationService,
|
private readonly jwtVerificationService: JwtVerificationService,
|
||||||
|
@Inject(forwardRef(() => FriendsService))
|
||||||
|
private readonly friendsService: FriendsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
afterInit() {
|
afterInit() {
|
||||||
@@ -83,6 +89,10 @@ export class StateGateway
|
|||||||
const user = await this.authService.syncUserFromToken(client.data.user);
|
const user = await this.authService.syncUserFromToken(client.data.user);
|
||||||
this.userSocketMap.set(user.id, client.id);
|
this.userSocketMap.set(user.id, client.id);
|
||||||
|
|
||||||
|
// Initialize friends cache
|
||||||
|
const friends = await this.friendsService.getFriends(user.id);
|
||||||
|
client.data.friends = new Set(friends.map((f) => f.friendId));
|
||||||
|
|
||||||
const { sockets } = this.io.sockets;
|
const { sockets } = this.io.sockets;
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Client id: ${client.id} connected (user: ${payload.sub})`,
|
`Client id: ${client.id} connected (user: ${payload.sub})`,
|
||||||
@@ -124,10 +134,45 @@ export class StateGateway
|
|||||||
throw new WsException('Unauthorized');
|
throw new WsException('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(
|
// Get the user ID from the userSocketMap (keycloakSub -> userId map is handled implicitly by connection logic but let's be safe and get it from map iteration or better store userId in client.data)
|
||||||
`Message received from client id: ${client.id} (user: ${user.keycloakSub})`,
|
// Actually we stored user.id -> client.id in userSocketMap. But we don't have direct access to user.id from client.data.user (it has keycloakSub).
|
||||||
);
|
// Let's improve this by finding the userId. The user is already synced in handleConnection.
|
||||||
this.logger.debug(`Payload: ${JSON.stringify(data, null, 0)}`);
|
// However, for efficiency, let's reverse lookup or better yet, assume we can get userId.
|
||||||
|
// In handleConnection we did: const user = await this.authService.syncUserFromToken(client.data.user); this.userSocketMap.set(user.id, client.id);
|
||||||
|
// So we know the user.id is in the map.
|
||||||
|
|
||||||
|
let currentUserId: string | undefined;
|
||||||
|
for (const [uid, sid] of this.userSocketMap.entries()) {
|
||||||
|
if (sid === client.id) {
|
||||||
|
currentUserId = uid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentUserId) {
|
||||||
|
this.logger.warn(`Could not find user ID for client ${client.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast to online friends
|
||||||
|
const friends = client.data.friends;
|
||||||
|
if (friends) {
|
||||||
|
for (const friendId of friends) {
|
||||||
|
const friendSocketId = this.userSocketMap.get(friendId);
|
||||||
|
if (friendSocketId) {
|
||||||
|
const payload = {
|
||||||
|
userId: currentUserId,
|
||||||
|
position: data,
|
||||||
|
};
|
||||||
|
this.logger.debug(
|
||||||
|
`Sending friend cursor position to user ${friendId}: ${JSON.stringify(payload, null, 0)}`,
|
||||||
|
);
|
||||||
|
this.io
|
||||||
|
.to(friendSocketId)
|
||||||
|
.emit(WS_EVENT.FRIEND_CURSOR_POSITION, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitFriendRequestReceived(
|
emitFriendRequestReceived(
|
||||||
@@ -158,6 +203,32 @@ export class StateGateway
|
|||||||
) {
|
) {
|
||||||
const socketId = this.userSocketMap.get(userId);
|
const socketId = this.userSocketMap.get(userId);
|
||||||
if (socketId) {
|
if (socketId) {
|
||||||
|
// Update cache for the user accepting (userId here is the sender of the original request)
|
||||||
|
// Wait, in friends.controller: acceptFriendRequest returns the request.
|
||||||
|
// emitFriendRequestAccepted is called with friendRequest.senderId (the one who sent the request).
|
||||||
|
// The one who accepted is friendRequest.receiverId.
|
||||||
|
|
||||||
|
// We need to update cache for BOTH users if they are online.
|
||||||
|
|
||||||
|
// 1. Update cache for the user who sent the request (userId / friendRequest.senderId)
|
||||||
|
const senderSocket = this.io.sockets.sockets.get(
|
||||||
|
socketId,
|
||||||
|
) as AuthenticatedSocket;
|
||||||
|
if (senderSocket && senderSocket.data.friends) {
|
||||||
|
senderSocket.data.friends.add(friendRequest.receiverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update cache for the user who accepted the request (friendRequest.receiverId)
|
||||||
|
const receiverSocketId = this.userSocketMap.get(friendRequest.receiverId);
|
||||||
|
if (receiverSocketId) {
|
||||||
|
const receiverSocket = this.io.sockets.sockets.get(
|
||||||
|
receiverSocketId,
|
||||||
|
) as AuthenticatedSocket;
|
||||||
|
if (receiverSocket && receiverSocket.data.friends) {
|
||||||
|
receiverSocket.data.friends.add(friendRequest.senderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.io.to(socketId).emit(WS_EVENT.FRIEND_REQUEST_ACCEPTED, {
|
this.io.to(socketId).emit(WS_EVENT.FRIEND_REQUEST_ACCEPTED, {
|
||||||
id: friendRequest.id,
|
id: friendRequest.id,
|
||||||
friend: {
|
friend: {
|
||||||
@@ -199,10 +270,47 @@ export class StateGateway
|
|||||||
emitUnfriended(userId: string, friendId: string) {
|
emitUnfriended(userId: string, friendId: string) {
|
||||||
const socketId = this.userSocketMap.get(userId);
|
const socketId = this.userSocketMap.get(userId);
|
||||||
if (socketId) {
|
if (socketId) {
|
||||||
|
// Update cache for the user being unfriended (userId)
|
||||||
|
// Wait, emitUnfriended is called with (friendId, user.id) in controller.
|
||||||
|
// So userId here is the friendId (the one being removed from friend list of the initiator).
|
||||||
|
// friendId here is the initiator (user.id).
|
||||||
|
|
||||||
|
// We need to update cache for BOTH users.
|
||||||
|
|
||||||
|
// 1. Update cache for the user receiving the notification (userId)
|
||||||
|
const socket = this.io.sockets.sockets.get(
|
||||||
|
socketId,
|
||||||
|
) as AuthenticatedSocket;
|
||||||
|
if (socket && socket.data.friends) {
|
||||||
|
socket.data.friends.delete(friendId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update cache for the user initiating the unfriend (friendId)
|
||||||
|
const initiatorSocketId = this.userSocketMap.get(friendId);
|
||||||
|
if (initiatorSocketId) {
|
||||||
|
const initiatorSocket = this.io.sockets.sockets.get(
|
||||||
|
initiatorSocketId,
|
||||||
|
) as AuthenticatedSocket;
|
||||||
|
if (initiatorSocket && initiatorSocket.data.friends) {
|
||||||
|
initiatorSocket.data.friends.delete(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.io.to(socketId).emit(WS_EVENT.UNFRIENDED, {
|
this.io.to(socketId).emit(WS_EVENT.UNFRIENDED, {
|
||||||
friendId,
|
friendId,
|
||||||
});
|
});
|
||||||
this.logger.debug(`Emitted unfriended notification to user ${userId}`);
|
this.logger.debug(`Emitted unfriended notification to user ${userId}`);
|
||||||
|
} else {
|
||||||
|
// If the notified user is offline, we still need to update the initiator's cache if they are online
|
||||||
|
const initiatorSocketId = this.userSocketMap.get(friendId);
|
||||||
|
if (initiatorSocketId) {
|
||||||
|
const initiatorSocket = this.io.sockets.sockets.get(
|
||||||
|
initiatorSocketId,
|
||||||
|
) as AuthenticatedSocket;
|
||||||
|
if (initiatorSocket && initiatorSocket.data.friends) {
|
||||||
|
initiatorSocket.data.friends.delete(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { StateGateway } from './state/state.gateway';
|
import { StateGateway } from './state/state.gateway';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { FriendsModule } from '../friends/friends.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule],
|
imports: [AuthModule, forwardRef(() => FriendsModule)],
|
||||||
providers: [StateGateway],
|
providers: [StateGateway],
|
||||||
exports: [StateGateway],
|
exports: [StateGateway],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user