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 { FriendsService } from './friends.service';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
@@ -7,7 +7,12 @@ import { UsersModule } from '../users/users.module';
|
||||
import { WsModule } from '../ws/ws.module';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, AuthModule, UsersModule, WsModule],
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
forwardRef(() => WsModule),
|
||||
],
|
||||
controllers: [FriendsController],
|
||||
providers: [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, // ServerToClientEvents
|
||||
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 {
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
@@ -14,7 +14,10 @@ import type { AuthenticatedSocket } from '../../types/socket';
|
||||
import { AuthService } from '../../auth/auth.service';
|
||||
import { JwtVerificationService } from '../../auth/services/jwt-verification.service';
|
||||
import { CursorPositionDto } from '../dto/cursor-position.dto';
|
||||
import { FriendRequestWithRelations } from '../../friends/friends.service';
|
||||
import {
|
||||
FriendRequestWithRelations,
|
||||
FriendsService,
|
||||
} from '../../friends/friends.service';
|
||||
|
||||
const WS_EVENT = {
|
||||
CURSOR_REPORT_POSITION: 'cursor-report-position',
|
||||
@@ -22,6 +25,7 @@ const WS_EVENT = {
|
||||
FRIEND_REQUEST_ACCEPTED: 'friend-request-accepted',
|
||||
FRIEND_REQUEST_DENIED: 'friend-request-denied',
|
||||
UNFRIENDED: 'unfriended',
|
||||
FRIEND_CURSOR_POSITION: 'friend-cursor-position',
|
||||
} as const;
|
||||
|
||||
@WebSocketGateway({
|
||||
@@ -41,6 +45,8 @@ export class StateGateway
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly jwtVerificationService: JwtVerificationService,
|
||||
@Inject(forwardRef(() => FriendsService))
|
||||
private readonly friendsService: FriendsService,
|
||||
) {}
|
||||
|
||||
afterInit() {
|
||||
@@ -83,6 +89,10 @@ export class StateGateway
|
||||
const user = await this.authService.syncUserFromToken(client.data.user);
|
||||
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;
|
||||
this.logger.log(
|
||||
`Client id: ${client.id} connected (user: ${payload.sub})`,
|
||||
@@ -124,10 +134,45 @@ export class StateGateway
|
||||
throw new WsException('Unauthorized');
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Message received from client id: ${client.id} (user: ${user.keycloakSub})`,
|
||||
);
|
||||
this.logger.debug(`Payload: ${JSON.stringify(data, null, 0)}`);
|
||||
// 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)
|
||||
// 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.
|
||||
// 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(
|
||||
@@ -158,6 +203,32 @@ export class StateGateway
|
||||
) {
|
||||
const socketId = this.userSocketMap.get(userId);
|
||||
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, {
|
||||
id: friendRequest.id,
|
||||
friend: {
|
||||
@@ -199,10 +270,47 @@ export class StateGateway
|
||||
emitUnfriended(userId: string, friendId: string) {
|
||||
const socketId = this.userSocketMap.get(userId);
|
||||
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, {
|
||||
friendId,
|
||||
});
|
||||
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 { AuthModule } from '../auth/auth.module';
|
||||
import { FriendsModule } from '../friends/friends.module';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
imports: [AuthModule, forwardRef(() => FriendsModule)],
|
||||
providers: [StateGateway],
|
||||
exports: [StateGateway],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user