- Added `UsersService` to manage user preferences in `EventsGateway`. - Enhanced online/offline broadcasting to respect user `showOnlineStatus` preference. - Updated `handleTyping` and `check_status` to verify user preferences before emitting events. - Abstracted status broadcasting logic into `broadcastStatus`.
221 lines
6.3 KiB
TypeScript
221 lines
6.3 KiB
TypeScript
import { forwardRef, Inject, Logger } from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import {
|
|
ConnectedSocket,
|
|
MessageBody,
|
|
OnGatewayConnection,
|
|
OnGatewayDisconnect,
|
|
OnGatewayInit,
|
|
SubscribeMessage,
|
|
WebSocketGateway,
|
|
WebSocketServer,
|
|
} from "@nestjs/websockets";
|
|
import { getIronSession } from "iron-session";
|
|
import { Server, Socket } from "socket.io";
|
|
import { getSessionOptions, SessionData } from "../auth/session.config";
|
|
import { JwtService } from "../crypto/services/jwt.service";
|
|
import { UsersService } from "../users/users.service";
|
|
|
|
@WebSocketGateway({
|
|
transports: ["websocket"],
|
|
cors: {
|
|
origin: (
|
|
origin: string,
|
|
callback: (err: Error | null, allow?: boolean) => void,
|
|
) => {
|
|
// Autoriser si pas d'origine (ex: app mobile ou serveur à serveur)
|
|
// ou si on est en développement local
|
|
if (
|
|
!origin ||
|
|
origin.includes("localhost") ||
|
|
origin.includes("127.0.0.1")
|
|
) {
|
|
callback(null, true);
|
|
return;
|
|
}
|
|
|
|
// En production, on peut restreindre via une variable d'environnement
|
|
const domainName = process.env.CORS_DOMAIN_NAME;
|
|
if (!domainName || domainName === "*") {
|
|
callback(null, true);
|
|
return;
|
|
}
|
|
|
|
const allowedOrigins = domainName.split(",").map((o) => o.trim());
|
|
if (allowedOrigins.includes(origin)) {
|
|
callback(null, true);
|
|
} else {
|
|
callback(new Error("Not allowed by CORS"));
|
|
}
|
|
},
|
|
credentials: true,
|
|
methods: ["GET", "POST"],
|
|
},
|
|
})
|
|
export class EventsGateway
|
|
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
|
|
{
|
|
@WebSocketServer()
|
|
server!: Server;
|
|
|
|
private readonly logger = new Logger(EventsGateway.name);
|
|
private readonly onlineUsers = new Map<string, Set<string>>(); // userId -> Set of socketIds
|
|
|
|
constructor(
|
|
private readonly jwtService: JwtService,
|
|
private readonly configService: ConfigService,
|
|
@Inject(forwardRef(() => UsersService))
|
|
private readonly usersService: UsersService,
|
|
) {}
|
|
|
|
afterInit(_server: Server) {
|
|
this.logger.log("WebSocket Gateway initialized");
|
|
}
|
|
|
|
async handleConnection(client: Socket) {
|
|
try {
|
|
// Simuler un objet Request/Response pour iron-session
|
|
const req: any = {
|
|
headers: client.handshake.headers,
|
|
};
|
|
const res: any = {
|
|
setHeader: () => {},
|
|
getHeader: () => {},
|
|
};
|
|
|
|
const session = await getIronSession<SessionData>(
|
|
req,
|
|
res,
|
|
getSessionOptions(this.configService.get("SESSION_PASSWORD") as string),
|
|
);
|
|
|
|
if (!session.accessToken) {
|
|
this.logger.warn(`Client ${client.id} unauthorized connection`);
|
|
// Permettre les connexions anonymes pour voir les commentaires en temps réel ?
|
|
// Pour l'instant on déconnecte car le système actuel semble exiger l'auth
|
|
client.disconnect();
|
|
return;
|
|
}
|
|
|
|
const payload = await this.jwtService.verifyJwt(session.accessToken);
|
|
if (!payload.sub) {
|
|
throw new Error("Invalid token payload: missing sub");
|
|
}
|
|
|
|
client.data.user = payload;
|
|
|
|
// Rejoindre une room personnelle pour les notifications
|
|
client.join(`user:${payload.sub}`);
|
|
|
|
// Gérer le statut en ligne
|
|
const userId = payload.sub as string;
|
|
|
|
if (!this.onlineUsers.has(userId)) {
|
|
this.onlineUsers.set(userId, new Set());
|
|
|
|
// Vérifier les préférences de l'utilisateur
|
|
const user = await this.usersService.findOne(userId);
|
|
if (user?.showOnlineStatus) {
|
|
this.broadcastStatus(userId, "online");
|
|
}
|
|
}
|
|
this.onlineUsers.get(userId)?.add(client.id);
|
|
|
|
this.logger.log(`Client connected: ${client.id} (User: ${payload.sub})`);
|
|
} catch (error) {
|
|
this.logger.error(`Connection error for client ${client.id}: ${error}`);
|
|
client.disconnect();
|
|
}
|
|
}
|
|
|
|
async handleDisconnect(client: Socket) {
|
|
const userId = client.data.user?.sub;
|
|
if (userId && this.onlineUsers.has(userId)) {
|
|
const sockets = this.onlineUsers.get(userId);
|
|
sockets?.delete(client.id);
|
|
if (sockets?.size === 0) {
|
|
this.onlineUsers.delete(userId);
|
|
|
|
const user = await this.usersService.findOne(userId);
|
|
if (user?.showOnlineStatus) {
|
|
this.broadcastStatus(userId, "offline");
|
|
}
|
|
}
|
|
}
|
|
this.logger.log(`Client disconnected: ${client.id}`);
|
|
}
|
|
|
|
broadcastStatus(userId: string, status: "online" | "offline") {
|
|
this.server.emit("user_status", { userId, status });
|
|
}
|
|
|
|
isUserOnline(userId: string): boolean {
|
|
return this.onlineUsers.has(userId);
|
|
}
|
|
|
|
@SubscribeMessage("join_content")
|
|
handleJoinContent(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() contentId: string,
|
|
) {
|
|
client.join(`content:${contentId}`);
|
|
this.logger.log(`Client ${client.id} joined content room: ${contentId}`);
|
|
}
|
|
|
|
@SubscribeMessage("leave_content")
|
|
handleLeaveContent(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() contentId: string,
|
|
) {
|
|
client.leave(`content:${contentId}`);
|
|
this.logger.log(`Client ${client.id} left content room: ${contentId}`);
|
|
}
|
|
|
|
@SubscribeMessage("typing")
|
|
async handleTyping(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() data: { recipientId: string; isTyping: boolean },
|
|
) {
|
|
const userId = client.data.user?.sub;
|
|
if (!userId) return;
|
|
|
|
// Optionnel: vérifier si l'utilisateur autorise le statut en ligne avant d'émettre "typing"
|
|
// ou si on considère que typing est une interaction directe qui outrepasse le statut.
|
|
// Instagram affiche "Typing..." même si le statut en ligne est désactivé si on est dans le chat.
|
|
// Mais par souci de cohérence avec "showOnlineStatus", on peut le vérifier.
|
|
const user = await this.usersService.findOne(userId);
|
|
if (!user?.showOnlineStatus) return;
|
|
|
|
this.server.to(`user:${data.recipientId}`).emit("user_typing", {
|
|
userId,
|
|
isTyping: data.isTyping,
|
|
});
|
|
}
|
|
|
|
@SubscribeMessage("check_status")
|
|
async handleCheckStatus(
|
|
@ConnectedSocket() _client: Socket,
|
|
@MessageBody() userId: string,
|
|
) {
|
|
const isOnline = this.onlineUsers.has(userId);
|
|
if (!isOnline) return { userId, status: "offline" };
|
|
|
|
const user = await this.usersService.findOne(userId);
|
|
if (!user?.showOnlineStatus) return { userId, status: "offline" };
|
|
|
|
return {
|
|
userId,
|
|
status: "online",
|
|
};
|
|
}
|
|
|
|
// Méthode utilitaire pour envoyer des messages à un utilisateur spécifique
|
|
sendToUser(userId: string, event: string, data: any) {
|
|
this.server.to(`user:${userId}`).emit(event, data);
|
|
}
|
|
|
|
sendToContent(contentId: string, event: string, data: any) {
|
|
this.server.to(`content:${contentId}`).emit(event, data);
|
|
}
|
|
}
|