feat: implement real-time chat with Socket.IO
Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
This commit is contained in:
514
backend/src/__tests__/socket.test.js
Normal file
514
backend/src/__tests__/socket.test.js
Normal file
@@ -0,0 +1,514 @@
|
||||
const http = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
const Client = require('socket.io-client');
|
||||
const { initializeSocket } = require('../socket');
|
||||
const { generateToken } = require('../utils/auth');
|
||||
const { prisma } = require('../utils/db');
|
||||
|
||||
describe('Socket.IO Server', () => {
|
||||
let httpServer;
|
||||
let io;
|
||||
let serverSocket;
|
||||
let clientSocket;
|
||||
let testUser;
|
||||
let testToken;
|
||||
const port = 3001;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create test user
|
||||
testUser = await prisma.user.create({
|
||||
data: {
|
||||
username: 'sockettest',
|
||||
email: 'sockettest@example.com',
|
||||
passwordHash: 'hashedpassword',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
},
|
||||
});
|
||||
|
||||
testToken = generateToken({ userId: testUser.id });
|
||||
|
||||
// Create HTTP server and initialize Socket.IO
|
||||
httpServer = http.createServer();
|
||||
io = initializeSocket(httpServer);
|
||||
|
||||
httpServer.listen(port);
|
||||
|
||||
// Wait for server to be ready
|
||||
await new Promise((resolve) => {
|
||||
httpServer.once('listening', resolve);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup test user
|
||||
await prisma.user.delete({
|
||||
where: { id: testUser.id },
|
||||
});
|
||||
|
||||
// Close server and client
|
||||
if (clientSocket) clientSocket.close();
|
||||
io.close();
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (clientSocket && clientSocket.connected) {
|
||||
clientSocket.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
test('should reject connection without token', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`);
|
||||
|
||||
clientSocket.on('connect_error', (error) => {
|
||||
expect(error.message).toBe('Authentication required');
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.on('connect', () => {
|
||||
done(new Error('Should not connect without token'));
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject connection with invalid token', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`, {
|
||||
auth: { token: 'invalid-token' },
|
||||
});
|
||||
|
||||
clientSocket.on('connect_error', (error) => {
|
||||
expect(error.message).toBe('Invalid token');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should accept connection with valid token', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
clientSocket.on('connect', () => {
|
||||
expect(clientSocket.connected).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.on('connect_error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event Rooms', () => {
|
||||
let testEvent;
|
||||
let testChatRoom;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create test event and chat room
|
||||
testEvent = await prisma.event.create({
|
||||
data: {
|
||||
name: 'Test Event',
|
||||
location: 'Test Location',
|
||||
startDate: new Date('2025-12-01'),
|
||||
endDate: new Date('2025-12-03'),
|
||||
},
|
||||
});
|
||||
|
||||
testChatRoom = await prisma.chatRoom.create({
|
||||
data: {
|
||||
eventId: testEvent.id,
|
||||
type: 'event',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup test data
|
||||
await prisma.chatRoom.delete({
|
||||
where: { id: testChatRoom.id },
|
||||
});
|
||||
await prisma.event.delete({
|
||||
where: { id: testEvent.id },
|
||||
});
|
||||
});
|
||||
|
||||
test('should join event room successfully', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
clientSocket.on('connect', () => {
|
||||
clientSocket.emit('join_event_room', { eventId: testEvent.id });
|
||||
});
|
||||
|
||||
clientSocket.on('active_users', (users) => {
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBeGreaterThan(0);
|
||||
const currentUser = users.find(u => u.userId === testUser.id);
|
||||
expect(currentUser).toBeDefined();
|
||||
expect(currentUser.username).toBe(testUser.username);
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.on('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
test('should receive user_joined notification', (done) => {
|
||||
const client1 = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
client1.on('connect', () => {
|
||||
client1.emit('join_event_room', { eventId: testEvent.id });
|
||||
|
||||
// Create second user and join the same room
|
||||
const client2Token = generateToken({ userId: testUser.id });
|
||||
const client2 = Client(`http://localhost:${port}`, {
|
||||
auth: { token: client2Token },
|
||||
});
|
||||
|
||||
client1.on('user_joined', (userData) => {
|
||||
expect(userData.userId).toBe(testUser.id);
|
||||
expect(userData.username).toBe(testUser.username);
|
||||
client1.close();
|
||||
client2.close();
|
||||
done();
|
||||
});
|
||||
|
||||
client2.on('connect', () => {
|
||||
client2.emit('join_event_room', { eventId: testEvent.id });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should send and receive event messages', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
const messageContent = 'Test message content';
|
||||
|
||||
clientSocket.on('connect', () => {
|
||||
clientSocket.emit('join_event_room', { eventId: testEvent.id });
|
||||
|
||||
clientSocket.on('active_users', () => {
|
||||
// Wait for active_users, then send message
|
||||
clientSocket.emit('send_event_message', {
|
||||
eventId: testEvent.id,
|
||||
content: messageContent,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
clientSocket.on('event_message', async (message) => {
|
||||
expect(message.content).toBe(messageContent);
|
||||
expect(message.userId).toBe(testUser.id);
|
||||
expect(message.username).toBe(testUser.username);
|
||||
expect(message.type).toBe('text');
|
||||
expect(message.createdAt).toBeDefined();
|
||||
|
||||
// Verify message was saved to database
|
||||
const dbMessage = await prisma.message.findUnique({
|
||||
where: { id: message.id },
|
||||
});
|
||||
expect(dbMessage).toBeDefined();
|
||||
expect(dbMessage.content).toBe(messageContent);
|
||||
|
||||
// Cleanup
|
||||
await prisma.message.delete({ where: { id: message.id } });
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.on('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
test('should leave event room and update active users', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
clientSocket.on('connect', () => {
|
||||
clientSocket.emit('join_event_room', { eventId: testEvent.id });
|
||||
|
||||
clientSocket.on('active_users', (users) => {
|
||||
if (users.length > 0) {
|
||||
// User joined, now leave
|
||||
clientSocket.emit('leave_event_room', { eventId: testEvent.id });
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Match Rooms', () => {
|
||||
let testUser2;
|
||||
let testMatch;
|
||||
let testMatchRoom;
|
||||
let testEvent;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create test event for match
|
||||
testEvent = await prisma.event.create({
|
||||
data: {
|
||||
name: 'Match Test Event',
|
||||
location: 'Test Location',
|
||||
startDate: new Date('2025-12-01'),
|
||||
endDate: new Date('2025-12-03'),
|
||||
},
|
||||
});
|
||||
|
||||
// Create second user with timestamp to avoid conflicts
|
||||
const timestamp = Date.now();
|
||||
testUser2 = await prisma.user.create({
|
||||
data: {
|
||||
username: `sockettest2_${timestamp}`,
|
||||
email: `sockettest2_${timestamp}@example.com`,
|
||||
passwordHash: 'hashedpassword',
|
||||
avatar: 'https://example.com/avatar2.jpg',
|
||||
},
|
||||
});
|
||||
|
||||
// Create match
|
||||
testMatch = await prisma.match.create({
|
||||
data: {
|
||||
status: 'active',
|
||||
user1: { connect: { id: testUser.id } },
|
||||
user2: { connect: { id: testUser2.id } },
|
||||
event: { connect: { id: testEvent.id } },
|
||||
},
|
||||
});
|
||||
|
||||
// Create match chat room
|
||||
testMatchRoom = await prisma.chatRoom.create({
|
||||
data: {
|
||||
type: 'private',
|
||||
},
|
||||
});
|
||||
|
||||
// Link room to match
|
||||
await prisma.match.update({
|
||||
where: { id: testMatch.id },
|
||||
data: { roomId: testMatchRoom.id },
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
await prisma.match.delete({
|
||||
where: { id: testMatch.id },
|
||||
});
|
||||
await prisma.chatRoom.delete({
|
||||
where: { id: testMatchRoom.id },
|
||||
});
|
||||
await prisma.user.delete({
|
||||
where: { id: testUser2.id },
|
||||
});
|
||||
await prisma.event.delete({
|
||||
where: { id: testEvent.id },
|
||||
});
|
||||
});
|
||||
|
||||
test('should join match room successfully', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
clientSocket.on('connect', () => {
|
||||
clientSocket.emit('join_match_room', { matchId: testMatch.id });
|
||||
// Just verify no error occurs
|
||||
setTimeout(() => {
|
||||
expect(clientSocket.connected).toBe(true);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
clientSocket.on('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
test('should send and receive match messages', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
const messageContent = 'Private match message';
|
||||
|
||||
clientSocket.on('connect', () => {
|
||||
clientSocket.emit('join_match_room', { matchId: testMatch.id });
|
||||
|
||||
setTimeout(() => {
|
||||
clientSocket.emit('send_match_message', {
|
||||
matchId: testMatch.id,
|
||||
content: messageContent,
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
clientSocket.on('match_message', async (message) => {
|
||||
expect(message.content).toBe(messageContent);
|
||||
expect(message.userId).toBe(testUser.id);
|
||||
expect(message.username).toBe(testUser.username);
|
||||
expect(message.type).toBe('text');
|
||||
|
||||
// Verify message was saved
|
||||
const dbMessage = await prisma.message.findUnique({
|
||||
where: { id: message.id },
|
||||
});
|
||||
expect(dbMessage).toBeDefined();
|
||||
expect(dbMessage.roomId).toBe(testMatchRoom.id);
|
||||
|
||||
// Cleanup
|
||||
await prisma.message.delete({ where: { id: message.id } });
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.on('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle match room not found error', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
clientSocket.on('connect', () => {
|
||||
clientSocket.emit('join_match_room', { matchId: 99999 });
|
||||
|
||||
setTimeout(() => {
|
||||
clientSocket.emit('send_match_message', {
|
||||
matchId: 99999,
|
||||
content: 'Test',
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
clientSocket.on('error', (error) => {
|
||||
expect(error.message).toBe('Match room not found');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disconnect Handling', () => {
|
||||
let testEvent;
|
||||
let testChatRoom;
|
||||
|
||||
beforeAll(async () => {
|
||||
testEvent = await prisma.event.create({
|
||||
data: {
|
||||
name: 'Disconnect Test Event',
|
||||
location: 'Test Location',
|
||||
startDate: new Date('2025-12-01'),
|
||||
endDate: new Date('2025-12-03'),
|
||||
},
|
||||
});
|
||||
|
||||
testChatRoom = await prisma.chatRoom.create({
|
||||
data: {
|
||||
eventId: testEvent.id,
|
||||
type: 'event',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.chatRoom.delete({
|
||||
where: { id: testChatRoom.id },
|
||||
});
|
||||
await prisma.event.delete({
|
||||
where: { id: testEvent.id },
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle disconnect and update active users', (done) => {
|
||||
const client1 = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
const client2Token = generateToken({ userId: testUser.id });
|
||||
const client2 = Client(`http://localhost:${port}`, {
|
||||
auth: { token: client2Token },
|
||||
});
|
||||
|
||||
let client1Connected = false;
|
||||
let client2Connected = false;
|
||||
|
||||
client1.on('connect', () => {
|
||||
client1Connected = true;
|
||||
client1.emit('join_event_room', { eventId: testEvent.id });
|
||||
});
|
||||
|
||||
client2.on('connect', () => {
|
||||
client2Connected = true;
|
||||
client2.emit('join_event_room', { eventId: testEvent.id });
|
||||
});
|
||||
|
||||
client2.on('user_left', (userData) => {
|
||||
expect(userData.userId).toBe(testUser.id);
|
||||
expect(userData.username).toBe(testUser.username);
|
||||
client2.close();
|
||||
done();
|
||||
});
|
||||
|
||||
// Wait for both to join, then disconnect client1
|
||||
setTimeout(() => {
|
||||
if (client1Connected && client2Connected) {
|
||||
client1.close();
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
let testEvent;
|
||||
|
||||
beforeAll(async () => {
|
||||
testEvent = await prisma.event.create({
|
||||
data: {
|
||||
name: 'Error Test Event',
|
||||
location: 'Test Location',
|
||||
startDate: new Date('2025-12-01'),
|
||||
endDate: new Date('2025-12-03'),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.event.delete({
|
||||
where: { id: testEvent.id },
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle chat room not found error', (done) => {
|
||||
clientSocket = Client(`http://localhost:${port}`, {
|
||||
auth: { token: testToken },
|
||||
});
|
||||
|
||||
clientSocket.on('connect', () => {
|
||||
clientSocket.emit('join_event_room', { eventId: testEvent.id });
|
||||
|
||||
setTimeout(() => {
|
||||
clientSocket.emit('send_event_message', {
|
||||
eventId: testEvent.id,
|
||||
content: 'Test message',
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
clientSocket.on('error', (error) => {
|
||||
expect(error.message).toBe('Chat room not found');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
require('dotenv').config();
|
||||
const http = require('http');
|
||||
const app = require('./app');
|
||||
const { testConnection, disconnect } = require('./utils/db');
|
||||
const { initializeSocket } = require('./socket');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
@@ -8,7 +10,13 @@ async function startServer() {
|
||||
// Test database connection
|
||||
await testConnection();
|
||||
|
||||
const server = app.listen(PORT, '0.0.0.0', () => {
|
||||
// Create HTTP server
|
||||
const server = http.createServer(app);
|
||||
|
||||
// Initialize Socket.IO
|
||||
initializeSocket(server);
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log('=================================');
|
||||
console.log('🚀 spotlight.cam Backend Started');
|
||||
console.log('=================================');
|
||||
|
||||
270
backend/src/socket/index.js
Normal file
270
backend/src/socket/index.js
Normal file
@@ -0,0 +1,270 @@
|
||||
const { Server } = require('socket.io');
|
||||
const { verifyToken } = require('../utils/auth');
|
||||
const { prisma } = require('../utils/db');
|
||||
|
||||
// Track active users in each event room
|
||||
const activeUsers = new Map(); // eventId -> Set of { socketId, userId, username, avatar }
|
||||
|
||||
function initializeSocket(httpServer) {
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:8080',
|
||||
credentials: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Authentication middleware for Socket.IO
|
||||
io.use(async (socket, next) => {
|
||||
try {
|
||||
const token = socket.handshake.auth.token;
|
||||
|
||||
if (!token) {
|
||||
return next(new Error('Authentication required'));
|
||||
}
|
||||
|
||||
const decoded = verifyToken(token);
|
||||
if (!decoded) {
|
||||
return next(new Error('Invalid token'));
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return next(new Error('User not found'));
|
||||
}
|
||||
|
||||
socket.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Socket auth error:', error);
|
||||
next(new Error('Authentication failed'));
|
||||
}
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`✅ User connected: ${socket.user.username} (${socket.id})`);
|
||||
|
||||
// Join event room
|
||||
socket.on('join_event_room', async ({ eventId }) => {
|
||||
try {
|
||||
const roomName = `event_${eventId}`;
|
||||
socket.join(roomName);
|
||||
socket.currentEventRoom = roomName;
|
||||
socket.currentEventId = eventId;
|
||||
|
||||
// Add user to active users
|
||||
if (!activeUsers.has(eventId)) {
|
||||
activeUsers.set(eventId, new Set());
|
||||
}
|
||||
|
||||
const userInfo = {
|
||||
socketId: socket.id,
|
||||
userId: socket.user.id,
|
||||
username: socket.user.username,
|
||||
avatar: socket.user.avatar,
|
||||
};
|
||||
|
||||
activeUsers.get(eventId).add(JSON.stringify(userInfo));
|
||||
|
||||
console.log(`👤 ${socket.user.username} joined event room ${eventId}`);
|
||||
|
||||
// Broadcast updated active users list
|
||||
const users = Array.from(activeUsers.get(eventId)).map(u => JSON.parse(u));
|
||||
io.to(roomName).emit('active_users', users);
|
||||
|
||||
// Notify room about new user
|
||||
socket.to(roomName).emit('user_joined', {
|
||||
userId: socket.user.id,
|
||||
username: socket.user.username,
|
||||
avatar: socket.user.avatar,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Join event room error:', error);
|
||||
socket.emit('error', { message: 'Failed to join room' });
|
||||
}
|
||||
});
|
||||
|
||||
// Leave event room
|
||||
socket.on('leave_event_room', ({ eventId }) => {
|
||||
const roomName = `event_${eventId}`;
|
||||
socket.leave(roomName);
|
||||
|
||||
// Remove from active users
|
||||
if (activeUsers.has(eventId)) {
|
||||
const users = activeUsers.get(eventId);
|
||||
const userInfo = JSON.stringify({
|
||||
socketId: socket.id,
|
||||
userId: socket.user.id,
|
||||
username: socket.user.username,
|
||||
avatar: socket.user.avatar,
|
||||
});
|
||||
users.delete(userInfo);
|
||||
|
||||
// Broadcast updated list
|
||||
const updatedUsers = Array.from(users).map(u => JSON.parse(u));
|
||||
io.to(roomName).emit('active_users', updatedUsers);
|
||||
}
|
||||
|
||||
console.log(`👤 ${socket.user.username} left event room ${eventId}`);
|
||||
});
|
||||
|
||||
// Send message to event room
|
||||
socket.on('send_event_message', async ({ eventId, content }) => {
|
||||
try {
|
||||
const roomName = `event_${eventId}`;
|
||||
|
||||
// Save message to database
|
||||
const chatRoom = await prisma.chatRoom.findFirst({
|
||||
where: {
|
||||
eventId: parseInt(eventId),
|
||||
type: 'event',
|
||||
},
|
||||
});
|
||||
|
||||
if (!chatRoom) {
|
||||
return socket.emit('error', { message: 'Chat room not found' });
|
||||
}
|
||||
|
||||
const message = await prisma.message.create({
|
||||
data: {
|
||||
roomId: chatRoom.id,
|
||||
userId: socket.user.id,
|
||||
content,
|
||||
type: 'text',
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Broadcast message to room
|
||||
io.to(roomName).emit('event_message', {
|
||||
id: message.id,
|
||||
roomId: message.roomId,
|
||||
userId: message.user.id,
|
||||
username: message.user.username,
|
||||
avatar: message.user.avatar,
|
||||
content: message.content,
|
||||
type: message.type,
|
||||
createdAt: message.createdAt,
|
||||
});
|
||||
|
||||
console.log(`💬 Message in event ${eventId} from ${socket.user.username}`);
|
||||
} catch (error) {
|
||||
console.error('Send message error:', error);
|
||||
socket.emit('error', { message: 'Failed to send message' });
|
||||
}
|
||||
});
|
||||
|
||||
// Join private match room
|
||||
socket.on('join_match_room', ({ matchId }) => {
|
||||
const roomName = `match_${matchId}`;
|
||||
socket.join(roomName);
|
||||
socket.currentMatchRoom = roomName;
|
||||
console.log(`👥 ${socket.user.username} joined match room ${matchId}`);
|
||||
});
|
||||
|
||||
// Send message to match room
|
||||
socket.on('send_match_message', async ({ matchId, content }) => {
|
||||
try {
|
||||
const roomName = `match_${matchId}`;
|
||||
|
||||
// Get match and its room
|
||||
const match = await prisma.match.findUnique({
|
||||
where: { id: parseInt(matchId) },
|
||||
include: { room: true },
|
||||
});
|
||||
|
||||
if (!match || !match.room) {
|
||||
return socket.emit('error', { message: 'Match room not found' });
|
||||
}
|
||||
|
||||
// Save message
|
||||
const message = await prisma.message.create({
|
||||
data: {
|
||||
roomId: match.room.id,
|
||||
userId: socket.user.id,
|
||||
content,
|
||||
type: 'text',
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Broadcast to match room
|
||||
io.to(roomName).emit('match_message', {
|
||||
id: message.id,
|
||||
roomId: message.roomId,
|
||||
userId: message.user.id,
|
||||
username: message.user.username,
|
||||
avatar: message.user.avatar,
|
||||
content: message.content,
|
||||
type: message.type,
|
||||
createdAt: message.createdAt,
|
||||
});
|
||||
|
||||
console.log(`💬 Private message in match ${matchId} from ${socket.user.username}`);
|
||||
} catch (error) {
|
||||
console.error('Send match message error:', error);
|
||||
socket.emit('error', { message: 'Failed to send message' });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`❌ User disconnected: ${socket.user.username} (${socket.id})`);
|
||||
|
||||
// Remove from active users in all event rooms
|
||||
if (socket.currentEventId) {
|
||||
const eventId = socket.currentEventId;
|
||||
if (activeUsers.has(eventId)) {
|
||||
const users = activeUsers.get(eventId);
|
||||
const userInfo = JSON.stringify({
|
||||
socketId: socket.id,
|
||||
userId: socket.user.id,
|
||||
username: socket.user.username,
|
||||
avatar: socket.user.avatar,
|
||||
});
|
||||
users.delete(userInfo);
|
||||
|
||||
// Broadcast updated list
|
||||
const updatedUsers = Array.from(users).map(u => JSON.parse(u));
|
||||
io.to(socket.currentEventRoom).emit('active_users', updatedUsers);
|
||||
|
||||
// Notify about user leaving
|
||||
socket.to(socket.currentEventRoom).emit('user_left', {
|
||||
userId: socket.user.id,
|
||||
username: socket.user.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log('🔌 Socket.IO initialized');
|
||||
return io;
|
||||
}
|
||||
|
||||
module.exports = { initializeSocket };
|
||||
Reference in New Issue
Block a user