2025-11-12 22:42:15 +01:00
|
|
|
const { Server } = require('socket.io');
|
|
|
|
|
const { verifyToken } = require('../utils/auth');
|
|
|
|
|
const { prisma } = require('../utils/db');
|
2025-12-02 20:07:10 +01:00
|
|
|
const { ACTIONS, log: activityLog } = require('../services/activityLog');
|
2025-11-12 22:42:15 +01:00
|
|
|
|
|
|
|
|
// Track active users in each event room
|
|
|
|
|
const activeUsers = new Map(); // eventId -> Set of { socketId, userId, username, avatar }
|
|
|
|
|
|
2025-11-14 15:35:39 +01:00
|
|
|
// Global Socket.IO instance
|
|
|
|
|
let io;
|
|
|
|
|
|
|
|
|
|
function getIO() {
|
|
|
|
|
if (!io) {
|
|
|
|
|
throw new Error('Socket.IO not initialized');
|
|
|
|
|
}
|
|
|
|
|
return io;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 22:42:15 +01:00
|
|
|
function initializeSocket(httpServer) {
|
2025-11-14 15:35:39 +01:00
|
|
|
io = new Server(httpServer, {
|
2025-11-12 22:42:15 +01:00
|
|
|
cors: {
|
|
|
|
|
origin: process.env.CORS_ORIGIN || 'http://localhost:8080',
|
|
|
|
|
credentials: true,
|
|
|
|
|
},
|
2025-11-21 21:53:51 +01:00
|
|
|
// Ping/pong heartbeat configuration
|
|
|
|
|
pingInterval: 25000, // Send ping every 25 seconds
|
|
|
|
|
pingTimeout: 60000, // Wait 60 seconds for pong before considering disconnected
|
|
|
|
|
// Allow upgrade from polling to websocket
|
|
|
|
|
transports: ['polling', 'websocket'],
|
|
|
|
|
allowUpgrades: true,
|
2025-11-12 22:42:15 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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})`);
|
|
|
|
|
|
2025-11-14 19:22:23 +01:00
|
|
|
// Join user's personal room for notifications
|
|
|
|
|
const userRoom = `user_${socket.user.id}`;
|
|
|
|
|
socket.join(userRoom);
|
|
|
|
|
console.log(`📬 ${socket.user.username} joined personal room: ${userRoom}`);
|
|
|
|
|
|
2025-12-02 20:10:47 +01:00
|
|
|
// Join admin activity logs room (admin-only)
|
|
|
|
|
socket.on('join_admin_activity_logs', async () => {
|
|
|
|
|
try {
|
|
|
|
|
// Verify admin status
|
|
|
|
|
const user = await prisma.user.findUnique({
|
|
|
|
|
where: { id: socket.user.id },
|
|
|
|
|
select: { isAdmin: true, username: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user || !user.isAdmin) {
|
|
|
|
|
socket.emit('error', { message: 'Admin access required' });
|
|
|
|
|
console.warn(`🚫 Non-admin ${socket.user.username} attempted to join admin_activity_logs`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket.join('admin_activity_logs');
|
|
|
|
|
console.log(`👑 Admin ${user.username} joined activity logs streaming room`);
|
|
|
|
|
socket.emit('admin_activity_logs_joined', { message: 'Successfully joined activity logs stream' });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Join admin activity logs error:', error);
|
|
|
|
|
socket.emit('error', { message: 'Failed to join admin activity logs' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Leave admin activity logs room
|
|
|
|
|
socket.on('leave_admin_activity_logs', () => {
|
|
|
|
|
socket.leave('admin_activity_logs');
|
|
|
|
|
console.log(`👑 Admin ${socket.user.username} left activity logs streaming room`);
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-12 22:42:15 +01:00
|
|
|
// Join event room
|
2025-11-13 21:43:58 +01:00
|
|
|
socket.on('join_event_room', async ({ slug }) => {
|
2025-11-12 22:42:15 +01:00
|
|
|
try {
|
2025-11-13 21:43:58 +01:00
|
|
|
// Find event by slug
|
|
|
|
|
const event = await prisma.event.findUnique({
|
|
|
|
|
where: { slug },
|
|
|
|
|
select: { id: true, slug: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!event) {
|
|
|
|
|
socket.emit('error', { message: 'Event not found' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const eventId = event.id;
|
2025-11-12 22:42:15 +01:00
|
|
|
const roomName = `event_${eventId}`;
|
|
|
|
|
|
2025-11-14 14:36:49 +01:00
|
|
|
// Verify that user has checked in to this event
|
|
|
|
|
const participant = await prisma.eventParticipant.findUnique({
|
2025-11-13 21:18:15 +01:00
|
|
|
where: {
|
|
|
|
|
userId_eventId: {
|
|
|
|
|
userId: socket.user.id,
|
2025-11-13 21:43:58 +01:00
|
|
|
eventId: eventId,
|
2025-11-13 21:18:15 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-14 14:36:49 +01:00
|
|
|
if (!participant) {
|
|
|
|
|
socket.emit('error', { message: 'You must check in to this event first' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket.join(roomName);
|
|
|
|
|
socket.currentEventRoom = roomName;
|
|
|
|
|
socket.currentEventId = eventId;
|
|
|
|
|
socket.currentEventSlug = slug;
|
|
|
|
|
|
2025-11-12 22:42:15 +01:00
|
|
|
// 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));
|
|
|
|
|
|
2025-11-13 21:43:58 +01:00
|
|
|
console.log(`👤 ${socket.user.username} joined event room ${slug} (ID: ${eventId})`);
|
2025-11-12 22:42:15 +01:00
|
|
|
|
2025-11-13 20:16:58 +01:00
|
|
|
// Load last 20 messages from database
|
|
|
|
|
const chatRoom = await prisma.chatRoom.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
eventId: parseInt(eventId),
|
|
|
|
|
type: 'event',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (chatRoom) {
|
|
|
|
|
const messages = await prisma.message.findMany({
|
|
|
|
|
where: { roomId: chatRoom.id },
|
|
|
|
|
include: {
|
|
|
|
|
user: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
username: true,
|
|
|
|
|
avatar: true,
|
2025-11-29 19:49:06 +01:00
|
|
|
country: true,
|
2025-11-13 20:16:58 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
take: 20,
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-29 19:49:06 +01:00
|
|
|
// Get competitor numbers for all users in this event
|
|
|
|
|
const userIds = [...new Set(messages.map(msg => msg.user.id))];
|
|
|
|
|
const eventParticipants = await prisma.eventParticipant.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
eventId: eventId,
|
|
|
|
|
userId: { in: userIds },
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
userId: true,
|
|
|
|
|
competitorNumber: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create a map of userId to competitorNumber
|
|
|
|
|
const competitorNumberMap = new Map(
|
|
|
|
|
eventParticipants.map(ep => [ep.userId, ep.competitorNumber])
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-13 20:16:58 +01:00
|
|
|
// Send message history to the joining user (reverse to chronological order)
|
|
|
|
|
socket.emit('message_history', messages.reverse().map(msg => ({
|
|
|
|
|
id: msg.id,
|
|
|
|
|
roomId: msg.roomId,
|
|
|
|
|
userId: msg.user.id,
|
|
|
|
|
content: msg.content,
|
|
|
|
|
type: msg.type,
|
|
|
|
|
createdAt: msg.createdAt,
|
2025-11-29 19:49:06 +01:00
|
|
|
// Nested user data for caching
|
|
|
|
|
user: {
|
|
|
|
|
id: msg.user.id,
|
|
|
|
|
username: msg.user.username,
|
|
|
|
|
avatar: msg.user.avatar,
|
|
|
|
|
country: msg.user.country,
|
|
|
|
|
},
|
|
|
|
|
// Nested participant data for caching
|
|
|
|
|
participant: {
|
|
|
|
|
competitorNumber: competitorNumberMap.get(msg.user.id),
|
|
|
|
|
},
|
2025-11-13 20:16:58 +01:00
|
|
|
})));
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 22:42:15 +01:00
|
|
|
// Broadcast updated active users list
|
|
|
|
|
const users = Array.from(activeUsers.get(eventId)).map(u => JSON.parse(u));
|
2025-12-02 23:38:46 +01:00
|
|
|
console.log(`📡 Emitting active_users to room ${roomName}:`, users.length, 'users');
|
2025-11-12 22:42:15 +01:00
|
|
|
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,
|
|
|
|
|
});
|
2025-12-02 20:07:10 +01:00
|
|
|
|
|
|
|
|
// Log join event chat activity
|
|
|
|
|
activityLog({
|
|
|
|
|
userId: socket.user.id,
|
|
|
|
|
username: socket.user.username,
|
|
|
|
|
ipAddress: socket.handshake.address,
|
|
|
|
|
action: ACTIONS.EVENT_JOIN_CHAT,
|
|
|
|
|
resource: `event:${eventId}`,
|
|
|
|
|
method: 'SOCKET',
|
|
|
|
|
path: 'join_event_room',
|
|
|
|
|
metadata: {
|
|
|
|
|
eventSlug: slug,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-11-12 22:42:15 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Join event room error:', error);
|
|
|
|
|
socket.emit('error', { message: 'Failed to join room' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Leave event room
|
2025-11-13 21:43:58 +01:00
|
|
|
socket.on('leave_event_room', () => {
|
|
|
|
|
if (!socket.currentEventId || !socket.currentEventRoom) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const eventId = socket.currentEventId;
|
|
|
|
|
const roomName = socket.currentEventRoom;
|
2025-11-12 22:42:15 +01:00
|
|
|
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));
|
2025-12-02 23:38:46 +01:00
|
|
|
console.log(`📡 Emitting active_users (after leave) to room ${roomName}:`, updatedUsers.length, 'users');
|
2025-11-12 22:42:15 +01:00
|
|
|
io.to(roomName).emit('active_users', updatedUsers);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-13 21:43:58 +01:00
|
|
|
console.log(`👤 ${socket.user.username} left event room ${socket.currentEventSlug}`);
|
|
|
|
|
|
2025-12-02 20:07:10 +01:00
|
|
|
// Log leave event chat activity
|
|
|
|
|
activityLog({
|
|
|
|
|
userId: socket.user.id,
|
|
|
|
|
username: socket.user.username,
|
|
|
|
|
ipAddress: socket.handshake.address,
|
|
|
|
|
action: ACTIONS.EVENT_LEAVE_CHAT,
|
|
|
|
|
resource: `event:${eventId}`,
|
|
|
|
|
method: 'SOCKET',
|
|
|
|
|
path: 'leave_event_room',
|
|
|
|
|
metadata: {
|
|
|
|
|
eventSlug: socket.currentEventSlug,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-13 21:43:58 +01:00
|
|
|
// Clear current event data
|
|
|
|
|
socket.currentEventId = null;
|
|
|
|
|
socket.currentEventRoom = null;
|
|
|
|
|
socket.currentEventSlug = null;
|
2025-11-12 22:42:15 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Send message to event room
|
2025-11-13 21:43:58 +01:00
|
|
|
socket.on('send_event_message', async ({ content }) => {
|
2025-11-12 22:42:15 +01:00
|
|
|
try {
|
2025-11-13 21:43:58 +01:00
|
|
|
if (!socket.currentEventId || !socket.currentEventRoom) {
|
|
|
|
|
return socket.emit('error', { message: 'Not in an event room' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const eventId = socket.currentEventId;
|
|
|
|
|
const roomName = socket.currentEventRoom;
|
2025-11-12 22:42:15 +01:00
|
|
|
|
|
|
|
|
// Save message to database
|
|
|
|
|
const chatRoom = await prisma.chatRoom.findFirst({
|
|
|
|
|
where: {
|
2025-11-13 21:43:58 +01:00
|
|
|
eventId: eventId,
|
2025-11-12 22:42:15 +01:00
|
|
|
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,
|
2025-11-29 19:49:06 +01:00
|
|
|
country: true,
|
2025-11-12 22:42:15 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-29 19:49:06 +01:00
|
|
|
// Get competitor number for this user in this event
|
|
|
|
|
const eventParticipant = await prisma.eventParticipant.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
userId_eventId: {
|
|
|
|
|
userId: socket.user.id,
|
|
|
|
|
eventId: eventId,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
competitorNumber: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-12 22:42:15 +01:00
|
|
|
// Broadcast message to room
|
|
|
|
|
io.to(roomName).emit('event_message', {
|
|
|
|
|
id: message.id,
|
|
|
|
|
roomId: message.roomId,
|
|
|
|
|
userId: message.user.id,
|
|
|
|
|
content: message.content,
|
|
|
|
|
type: message.type,
|
|
|
|
|
createdAt: message.createdAt,
|
2025-11-29 19:49:06 +01:00
|
|
|
// Nested user data for caching
|
|
|
|
|
user: {
|
|
|
|
|
id: message.user.id,
|
|
|
|
|
username: message.user.username,
|
|
|
|
|
avatar: message.user.avatar,
|
|
|
|
|
country: message.user.country,
|
|
|
|
|
},
|
|
|
|
|
// Nested participant data for caching
|
|
|
|
|
participant: {
|
|
|
|
|
competitorNumber: eventParticipant?.competitorNumber,
|
|
|
|
|
},
|
2025-11-12 22:42:15 +01:00
|
|
|
});
|
|
|
|
|
|
2025-11-13 21:43:58 +01:00
|
|
|
console.log(`💬 Message in event ${socket.currentEventSlug} from ${socket.user.username}`);
|
2025-11-12 22:42:15 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Send message error:', error);
|
|
|
|
|
socket.emit('error', { message: 'Failed to send message' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Join private match room
|
2025-11-21 21:46:00 +01:00
|
|
|
socket.on('join_match_room', async ({ matchId }) => {
|
|
|
|
|
try {
|
|
|
|
|
const roomName = `match_${matchId}`;
|
|
|
|
|
socket.join(roomName);
|
|
|
|
|
socket.currentMatchRoom = roomName;
|
|
|
|
|
socket.currentMatchId = parseInt(matchId);
|
|
|
|
|
console.log(`👥 ${socket.user.username} joined match room ${matchId}`);
|
|
|
|
|
|
|
|
|
|
// Mark messages as read by updating lastReadAt
|
|
|
|
|
const match = await prisma.match.findUnique({
|
|
|
|
|
where: { id: parseInt(matchId) },
|
|
|
|
|
select: { user1Id: true, user2Id: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
|
const isUser1 = match.user1Id === socket.user.id;
|
|
|
|
|
const updateData = isUser1
|
|
|
|
|
? { user1LastReadAt: new Date() }
|
|
|
|
|
: { user2LastReadAt: new Date() };
|
|
|
|
|
|
|
|
|
|
await prisma.match.update({
|
|
|
|
|
where: { id: parseInt(matchId) },
|
|
|
|
|
data: updateData,
|
|
|
|
|
});
|
2025-12-02 20:07:10 +01:00
|
|
|
|
|
|
|
|
// Log join match room activity
|
|
|
|
|
activityLog({
|
|
|
|
|
userId: socket.user.id,
|
|
|
|
|
username: socket.user.username,
|
|
|
|
|
ipAddress: socket.handshake.address,
|
|
|
|
|
action: ACTIONS.CHAT_JOIN_ROOM,
|
|
|
|
|
resource: `match:${matchId}`,
|
|
|
|
|
method: 'SOCKET',
|
|
|
|
|
path: 'join_match_room',
|
|
|
|
|
metadata: {
|
|
|
|
|
matchId: parseInt(matchId),
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-11-21 21:46:00 +01:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Join match room error:', error);
|
|
|
|
|
socket.emit('error', { message: 'Failed to join match room' });
|
|
|
|
|
}
|
2025-11-12 22:42:15 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-15 14:12:51 +01:00
|
|
|
// WebRTC Signaling Events
|
|
|
|
|
|
|
|
|
|
// Send WebRTC offer
|
|
|
|
|
socket.on('webrtc_offer', async ({ matchId, offer }) => {
|
|
|
|
|
try {
|
|
|
|
|
const roomName = `match_${matchId}`;
|
|
|
|
|
|
|
|
|
|
// Verify user is part of this match
|
|
|
|
|
const match = await prisma.match.findUnique({
|
|
|
|
|
where: { id: parseInt(matchId) },
|
|
|
|
|
select: { user1Id: true, user2Id: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
return socket.emit('error', { message: 'Match not found' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (match.user1Id !== socket.user.id && match.user2Id !== socket.user.id) {
|
|
|
|
|
return socket.emit('error', { message: 'Not authorized for this match' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Forward offer to the other user in the match room
|
|
|
|
|
socket.to(roomName).emit('webrtc_offer', {
|
|
|
|
|
from: socket.user.id,
|
|
|
|
|
offer,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`📡 WebRTC offer sent in match ${matchId} from ${socket.user.username}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('WebRTC offer error:', error);
|
|
|
|
|
socket.emit('error', { message: 'Failed to send WebRTC offer' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Send WebRTC answer
|
|
|
|
|
socket.on('webrtc_answer', async ({ matchId, answer }) => {
|
|
|
|
|
try {
|
|
|
|
|
const roomName = `match_${matchId}`;
|
|
|
|
|
|
|
|
|
|
// Verify user is part of this match
|
|
|
|
|
const match = await prisma.match.findUnique({
|
|
|
|
|
where: { id: parseInt(matchId) },
|
|
|
|
|
select: { user1Id: true, user2Id: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
return socket.emit('error', { message: 'Match not found' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (match.user1Id !== socket.user.id && match.user2Id !== socket.user.id) {
|
|
|
|
|
return socket.emit('error', { message: 'Not authorized for this match' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Forward answer to the other user in the match room
|
|
|
|
|
socket.to(roomName).emit('webrtc_answer', {
|
|
|
|
|
from: socket.user.id,
|
|
|
|
|
answer,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`📡 WebRTC answer sent in match ${matchId} from ${socket.user.username}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('WebRTC answer error:', error);
|
|
|
|
|
socket.emit('error', { message: 'Failed to send WebRTC answer' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Send ICE candidate
|
|
|
|
|
socket.on('webrtc_ice_candidate', async ({ matchId, candidate }) => {
|
|
|
|
|
try {
|
|
|
|
|
const roomName = `match_${matchId}`;
|
|
|
|
|
|
|
|
|
|
// Verify user is part of this match
|
|
|
|
|
const match = await prisma.match.findUnique({
|
|
|
|
|
where: { id: parseInt(matchId) },
|
|
|
|
|
select: { user1Id: true, user2Id: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
return socket.emit('error', { message: 'Match not found' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (match.user1Id !== socket.user.id && match.user2Id !== socket.user.id) {
|
|
|
|
|
return socket.emit('error', { message: 'Not authorized for this match' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Forward ICE candidate to the other user in the match room
|
|
|
|
|
socket.to(roomName).emit('webrtc_ice_candidate', {
|
|
|
|
|
from: socket.user.id,
|
|
|
|
|
candidate,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`🧊 ICE candidate sent in match ${matchId} from ${socket.user.username}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('ICE candidate error:', error);
|
|
|
|
|
socket.emit('error', { message: 'Failed to send ICE candidate' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-12 22:42:15 +01:00
|
|
|
// 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));
|
2025-12-02 23:38:46 +01:00
|
|
|
console.log(`📡 Emitting active_users (after disconnect) to room ${socket.currentEventRoom}:`, updatedUsers.length, 'users');
|
2025-11-12 22:42:15 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-21 21:41:16 +01:00
|
|
|
// Get count of online users for a specific event
|
|
|
|
|
function getEventOnlineCount(eventId) {
|
|
|
|
|
if (!activeUsers.has(eventId)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return activeUsers.get(eventId).size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get online counts for multiple events
|
|
|
|
|
function getEventsOnlineCounts(eventIds) {
|
|
|
|
|
const counts = {};
|
|
|
|
|
for (const eventId of eventIds) {
|
|
|
|
|
counts[eventId] = getEventOnlineCount(eventId);
|
|
|
|
|
}
|
|
|
|
|
return counts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { initializeSocket, getIO, getEventOnlineCount, getEventsOnlineCounts };
|