import { Server, Socket } from 'socket.io'; import jwt from 'jsonwebtoken'; import config from '../config/environment'; import { messengerService } from '../services/messengerService'; import { JwtPayload } from '../types/auth'; interface AuthenticatedSocket extends Socket { data: { userId: string; userName: string; companyCode: string; }; } // In-memory presence store: userId → { companyCode, status } const presenceStore = new Map(); export function initMessengerSocket(io: Server) { // JWT authentication middleware io.use((socket, next) => { const token = socket.handshake.auth?.token || socket.handshake.query?.token; if (!token) { return next(new Error('Authentication required')); } try { const decoded = jwt.verify(token as string, config.jwt.secret) as JwtPayload; socket.data.userId = decoded.userId; socket.data.userName = decoded.userName; socket.data.companyCode = decoded.companyCode || ''; next(); } catch (error) { next(new Error('Invalid token')); } }); io.on('connection', async (socket: AuthenticatedSocket) => { const { userId, companyCode } = socket.data; console.log(`[Messenger] User connected: ${userId}`); // Join company presence room and broadcast online status const presenceRoom = `${companyCode}:presence`; socket.join(presenceRoom); presenceStore.set(userId, { companyCode, status: 'online' }); socket.to(presenceRoom).emit('user_status', { userId, status: 'online' }); // Send current online users list to newly connected socket const currentPresence: Record = {}; for (const [uid, info] of presenceStore.entries()) { if (info.companyCode === companyCode) { currentPresence[uid] = info.status; } } socket.emit('presence_list', currentPresence); // set_status: client emits when tab focus changes socket.on('set_status', (data: { status: 'online' | 'away' }) => { const entry = presenceStore.get(userId); if (entry) { entry.status = data.status; io.to(presenceRoom).emit('user_status', { userId, status: data.status }); } }); // join_rooms: subscribe to all user's rooms socket.on('join_rooms', async () => { try { const roomIds = await messengerService.getUserRoomIds(userId, companyCode); for (const roomId of roomIds) { socket.join(`${companyCode}:${roomId}`); } socket.emit('rooms_joined', { roomIds }); } catch (error) { console.error('[Messenger] join_rooms error:', error); socket.emit('error', { message: 'Failed to join rooms' }); } }); // send_message: save and broadcast socket.on('send_message', async (data: { room_id: number; content: string; message_type?: string; parent_message_id?: number; }) => { try { const message = await messengerService.sendMessage( data.room_id, userId, companyCode, data.content, data.message_type || 'text', data.parent_message_id ); io.to(`${companyCode}:${data.room_id}`).emit('new_message', message); } catch (error) { console.error('[Messenger] send_message error:', error); socket.emit('error', { message: 'Failed to send message' }); } }); // message_read: update last_read_at socket.on('message_read', async (data: { room_id: number }) => { try { await messengerService.markAsRead(data.room_id, userId); io.to(`${companyCode}:${data.room_id}`).emit('user_read', { room_id: data.room_id, user_id: userId, read_at: new Date().toISOString(), }); } catch (error) { console.error('[Messenger] message_read error:', error); } }); // typing indicators socket.on('typing_start', (data: { room_id: number }) => { socket.to(`${companyCode}:${data.room_id}`).emit('user_typing', { room_id: data.room_id, user_id: userId, user_name: socket.data.userName, }); }); socket.on('typing_stop', (data: { room_id: number }) => { socket.to(`${companyCode}:${data.room_id}`).emit('user_stop_typing', { room_id: data.room_id, user_id: userId, user_name: socket.data.userName, }); }); // reactions socket.on('add_reaction', async (data: { message_id: number; emoji: string; room_id: number }) => { try { await messengerService.addReaction(data.message_id, userId, data.emoji); io.to(`${companyCode}:${data.room_id}`).emit('reaction_added', { message_id: data.message_id, user_id: userId, emoji: data.emoji, }); } catch (error) { console.error('[Messenger] add_reaction error:', error); } }); socket.on('remove_reaction', async (data: { message_id: number; emoji: string; room_id: number }) => { try { await messengerService.removeReaction(data.message_id, userId, data.emoji); io.to(`${companyCode}:${data.room_id}`).emit('reaction_removed', { message_id: data.message_id, user_id: userId, emoji: data.emoji, }); } catch (error) { console.error('[Messenger] remove_reaction error:', error); } }); // join a specific room (e.g., after creating a new room) socket.on('join_room', (data: { room_id: number }) => { socket.join(`${companyCode}:${data.room_id}`); }); socket.on('disconnect', () => { console.log(`[Messenger] User disconnected: ${userId}`); presenceStore.delete(userId); io.to(presenceRoom).emit('user_status', { userId, status: 'offline' }); }); }); }