"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketService = void 0; const socket_io_1 = require("socket.io"); const JWTService_1 = require("./JWTService"); const ChatRepository_1 = require("../../Infrastructure/Repository/ChatRepository"); const ChatArchiveRepository_1 = require("../../Infrastructure/Repository/ChatArchiveRepository"); const UserRepository_1 = require("../../Infrastructure/Repository/UserRepository"); const ChatAggregate_1 = require("../../Domain/Chat/ChatAggregate"); const UserAggregate_1 = require("../../Domain/User/UserAggregate"); const Logger_1 = require("./Logger"); const RedisService_1 = require("./RedisService"); const uuid_1 = require("uuid"); class WebSocketService { constructor(httpServer) { this.connectedUsers = new Map(); this.userMessageCounts = new Map(); this.io = new socket_io_1.Server(httpServer, { cors: { origin: ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080'], methods: ['GET', 'POST'], credentials: true } }); this.jwtService = new JWTService_1.JWTService(); this.chatRepository = new ChatRepository_1.ChatRepository(); this.chatArchiveRepository = new ChatArchiveRepository_1.ChatArchiveRepository(); this.userRepository = new UserRepository_1.UserRepository(); this.redisService = RedisService_1.RedisService.getInstance(); this.chatTimeout = parseInt(process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'); this.maxMessagesPerUser = parseInt(process.env.CHAT_MAX_MESSAGES_PER_USER || '100'); this.messageCleanupWeeks = parseInt(process.env.CHAT_MESSAGE_CLEANUP_WEEKS || '4'); // Initialize Redis connection this.initializeRedis(); this.setupSocketHandlers(); this.setupArchivingScheduler(); (0, Logger_1.logRequest)('WebSocket service initialized', undefined, undefined, { chatTimeoutMinutes: this.chatTimeout }); } async initializeRedis() { try { await this.redisService.connect(); } catch (error) { (0, Logger_1.logError)('Failed to initialize Redis connection', error); } } setupSocketHandlers() { this.io.use(async (socket, next) => { try { const token = socket.handshake.auth.token || socket.handshake.headers.cookie ?.split(';') .find(c => c.trim().startsWith('auth_token=')) ?.split('=')[1]; if (!token) { (0, Logger_1.logWarning)('WebSocket connection rejected - No token provided', { socketId: socket.id, ip: socket.handshake.address }); return next(new Error('Authentication required')); } // Create a mock request object for JWT verification const mockRequest = { headers: { authorization: `Bearer ${token}`, cookie: `auth_token=${token}` }, cookies: { auth_token: token } }; const payload = this.jwtService.verify(mockRequest); if (!payload) { (0, Logger_1.logWarning)('WebSocket connection rejected - Invalid token', { socketId: socket.id, ip: socket.handshake.address }); return next(new Error('Invalid token')); } socket.userId = payload.userId; socket.authLevel = payload.authLevel; socket.userStatus = payload.userStatus; socket.orgId = payload.orgId; (0, Logger_1.logAuth)('WebSocket connection authenticated', payload.userId, { socketId: socket.id, authLevel: payload.authLevel, userStatus: payload.userStatus, orgId: payload.orgId }); next(); } catch (error) { (0, Logger_1.logError)('WebSocket authentication error', error); next(new Error('Authentication failed')); } }); this.io.on('connection', (socket) => { this.handleConnection(socket); }); } async handleConnection(socket) { const userId = socket.userId; // Store connected user this.connectedUsers.set(userId, socket); // Load user's active chats and join rooms try { const userChats = await this.chatRepository.findActiveChatsForUser(userId); const chatIds = userChats.map(chat => chat.id); // Join all chat rooms chatIds.forEach(chatId => { socket.join(chatId); }); // Store user's chat memberships in Redis await this.redisService.setActiveUser(userId, { userId, activeChatIds: chatIds, lastActivity: new Date(), isOnline: true }); // Also store each active chat in Redis for (const chat of userChats) { await this.redisService.setActiveChat(chat.id, { chatId: chat.id, participants: chat.users, lastActivity: chat.lastActivity || new Date(), messageCount: chat.messages.length, chatType: chat.type, gameId: chat.gameId || undefined, name: chat.name || undefined }); } (0, Logger_1.logAuth)('User connected to WebSocket', userId, { socketId: socket.id, activeChats: chatIds.length }); // Send user their active chats with unread counts const chatsWithUnread = await Promise.all(userChats.map(async (chat) => ({ id: chat.id, type: chat.type, name: chat.name, gameId: chat.gameId, users: chat.users, lastActivity: chat.lastActivity, unreadCount: this.calculateUnreadMessages(chat, userId), isArchived: false }))); socket.emit('chats:list', chatsWithUnread); } catch (error) { (0, Logger_1.logError)('Error loading user chats on connection', error, undefined, undefined); socket.emit('error', { message: 'Failed to load chats' }); } // Setup event handlers socket.on('chat:join', (data) => this.handleJoinChat(socket, data)); socket.on('chat:leave', (data) => this.handleLeaveChat(socket, data)); socket.on('message:send', (data) => this.handleSendMessage(socket, data)); socket.on('group:create', (data) => this.handleCreateGroup(socket, data)); socket.on('chat:direct', (data) => this.handleCreateDirectChat(socket, data)); socket.on('game:chat:create', (data) => this.handleCreateGameChat(socket, data)); socket.on('chat:history', (data) => this.handleGetChatHistory(socket, data)); socket.on('chat:delete', (data) => this.handleDeleteChat(socket, data)); socket.on('chat:archive:delete', (data) => this.handleDeleteChatArchive(socket, data)); socket.on('message:delete', (data) => this.handleDeleteMessage(socket, data)); socket.on('disconnect', () => this.handleDisconnection(socket)); } async handleJoinChat(socket, data) { try { const userId = socket.userId; const chat = await this.chatRepository.findById(data.chatId); if (!chat) { socket.emit('error', { message: 'Chat not found' }); return; } // Check if user is member of this chat if (!chat.users.includes(userId)) { socket.emit('error', { message: 'Unauthorized to join this chat' }); return; } // Join the chat room socket.join(data.chatId); // Add to user's active chats in Redis await this.redisService.addUserToChat(userId, data.chatId); // Update chat activity in Redis await this.redisService.updateChatActivity(data.chatId); // Update last activity in database await this.chatRepository.update(data.chatId, { lastActivity: new Date() }); (0, Logger_1.logAuth)('User joined chat', userId, { chatId: data.chatId, chatType: chat.type }); socket.emit('chat:joined', { chatId: data.chatId, messages: chat.messages.slice(-10) // Last 10 messages }); } catch (error) { (0, Logger_1.logError)('Error joining chat', error); socket.emit('error', { message: 'Failed to join chat' }); } } async handleLeaveChat(socket, data) { try { const userId = socket.userId; // Leave the chat room socket.leave(data.chatId); // Remove from user's active chats in Redis await this.redisService.removeUserFromChat(userId, data.chatId); (0, Logger_1.logAuth)('User left chat', userId, { chatId: data.chatId }); socket.emit('chat:left', { chatId: data.chatId }); } catch (error) { (0, Logger_1.logError)('Error leaving chat', error); socket.emit('error', { message: 'Failed to leave chat' }); } } async handleSendMessage(socket, data) { try { const userId = socket.userId; // Rate limiting check if (!this.checkMessageRateLimit(userId)) { socket.emit('error', { message: `Rate limit exceeded. Maximum ${this.maxMessagesPerUser} messages per minute allowed.` }); return; } // Validate message is string and not empty if (typeof data.message !== 'string' || !data.message.trim()) { socket.emit('error', { message: 'Message must be a non-empty string' }); return; } const chat = await this.chatRepository.findById(data.chatId); if (!chat) { socket.emit('error', { message: 'Chat not found' }); return; } // Check if user is member of this chat if (!chat.users.includes(userId)) { socket.emit('error', { message: 'Unauthorized to send message to this chat' }); return; } // Create message const message = { id: (0, uuid_1.v4)(), date: new Date(), userid: userId, text: data.message.trim() }; // Manage message history based on chat type let updatedMessages = [...chat.messages, message]; updatedMessages = this.pruneMessages(updatedMessages, chat.type); // Update chat await this.chatRepository.update(data.chatId, { messages: updatedMessages, lastActivity: new Date() }); // Update chat activity in Redis with new message count await this.redisService.updateChatActivity(data.chatId, updatedMessages.length); // Broadcast to all users in the chat room this.io.to(data.chatId).emit('message:received', { chatId: data.chatId, message: message }); // Send notifications to offline users await this.notifyOfflineUsers(chat, message); (0, Logger_1.logAuth)('Message sent', userId, { chatId: data.chatId, messageLength: data.message.length, chatType: chat.type }); } catch (error) { (0, Logger_1.logError)('Error sending message', error); socket.emit('error', { message: 'Failed to send message' }); } } async handleCreateGroup(socket, data) { try { const userId = socket.userId; // Check if user is premium (required to create groups) const user = await this.userRepository.findById(userId); if (!user || user.state !== UserAggregate_1.UserState.VERIFIED_PREMIUM) { socket.emit('error', { message: 'Premium subscription required to create groups' }); return; } // Validate group data if (!data.name?.trim()) { socket.emit('error', { message: 'Group name is required' }); return; } if (!data.userIds || data.userIds.length === 0) { socket.emit('error', { message: 'At least one member is required' }); return; } // Verify all users exist const members = await Promise.all(data.userIds.map(id => this.userRepository.findById(id))); if (members.some(member => !member)) { socket.emit('error', { message: 'One or more users not found' }); return; } // Create group chat const groupChat = await this.chatRepository.create({ type: ChatAggregate_1.ChatType.GROUP, name: data.name.trim(), createdBy: userId, users: [userId, ...data.userIds], // Include creator messages: [], lastActivity: new Date() }); // Add all members to the group room and store in Redis const allMemberIds = data.userIds.concat(userId); for (const memberId of allMemberIds) { const memberSocket = this.connectedUsers.get(memberId); if (memberSocket) { memberSocket.join(groupChat.id); } // Update user's chat list in Redis await this.redisService.addUserToChat(memberId, groupChat.id); } // Store the group chat in Redis await this.redisService.setActiveChat(groupChat.id, { chatId: groupChat.id, participants: allMemberIds, lastActivity: new Date(), messageCount: 0, chatType: 'group', name: groupChat.name || undefined }); // Notify all members this.io.to(groupChat.id).emit('group:created', { chat: { id: groupChat.id, type: groupChat.type, name: groupChat.name, createdBy: groupChat.createdBy, users: groupChat.users, messages: [] } }); (0, Logger_1.logAuth)('Group created', userId, { groupId: groupChat.id, groupName: data.name, memberCount: groupChat.users.length }); } catch (error) { (0, Logger_1.logError)('Error creating group', error); socket.emit('error', { message: 'Failed to create group' }); } } async handleCreateDirectChat(socket, data) { try { const userId = socket.userId; // Validate target user exists const targetUser = await this.userRepository.findById(data.targetUserId); if (!targetUser) { socket.emit('error', { message: 'Target user not found' }); return; } // Check if direct chat already exists const existingChats = await this.chatRepository.findByUserId(userId); const existingDirectChat = existingChats.find(chat => chat.type === ChatAggregate_1.ChatType.DIRECT && chat.users.length === 2 && chat.users.includes(data.targetUserId)); if (existingDirectChat) { socket.emit('chat:direct:exists', { chatId: existingDirectChat.id }); return; } // Create direct chat const directChat = await this.chatRepository.create({ type: ChatAggregate_1.ChatType.DIRECT, users: [userId, data.targetUserId], messages: [], lastActivity: new Date() }); // Add both users to the chat room if they're online and store in Redis const memberIds = [userId, data.targetUserId]; for (const memberId of memberIds) { const memberSocket = this.connectedUsers.get(memberId); if (memberSocket) { memberSocket.join(directChat.id); } // Update user's chat list in Redis await this.redisService.addUserToChat(memberId, directChat.id); } // Store the direct chat in Redis await this.redisService.setActiveChat(directChat.id, { chatId: directChat.id, participants: memberIds, lastActivity: new Date(), messageCount: 0, chatType: 'direct' }); // Notify both users this.io.to(directChat.id).emit('chat:direct:created', { chat: { id: directChat.id, type: directChat.type, users: directChat.users, messages: [] } }); (0, Logger_1.logAuth)('Direct chat created', userId, { chatId: directChat.id, targetUserId: data.targetUserId }); } catch (error) { (0, Logger_1.logError)('Error creating direct chat', error); socket.emit('error', { message: 'Failed to create direct chat' }); } } async handleCreateGameChat(socket, data) { try { const userId = socket.userId; // Check if game chat already exists const existingGameChat = await this.chatRepository.findByGameId(data.gameId); if (existingGameChat) { socket.emit('game:chat:exists', { chatId: existingGameChat.id }); return; } // Create game chat const gameChat = await this.chatRepository.create({ type: ChatAggregate_1.ChatType.GAME, name: data.gameName, gameId: data.gameId, users: data.playerIds, messages: [], lastActivity: new Date() }); // Add all players to the game chat room if they're online and store in Redis for (const playerId of data.playerIds) { const playerSocket = this.connectedUsers.get(playerId); if (playerSocket) { playerSocket.join(gameChat.id); } // Update user's chat list in Redis await this.redisService.addUserToChat(playerId, gameChat.id); } // Store the game chat in Redis await this.redisService.setActiveChat(gameChat.id, { chatId: gameChat.id, participants: data.playerIds, lastActivity: new Date(), messageCount: 0, chatType: 'game', gameId: gameChat.gameId || undefined, name: gameChat.name || undefined }); // Notify all players this.io.to(gameChat.id).emit('game:chat:created', { chat: { id: gameChat.id, type: gameChat.type, name: gameChat.name, gameId: gameChat.gameId, users: gameChat.users, messages: [] } }); (0, Logger_1.logAuth)('Game chat created', userId, { chatId: gameChat.id, gameId: data.gameId, gameName: data.gameName, playerCount: data.playerIds.length }); } catch (error) { (0, Logger_1.logError)('Error creating game chat', error); socket.emit('error', { message: 'Failed to create game chat' }); } } async handleGetChatHistory(socket, data) { try { const userId = socket.userId; const chat = await this.chatRepository.findById(data.chatId); if (!chat) { // Check if it's archived const archived = await this.chatRepository.getArchivedChat(data.chatId); if (archived) { socket.emit('chat:history:archived', { chatId: data.chatId, messages: archived.archivedMessages, chatType: archived.chatType, isGameChat: archived.chatType === ChatAggregate_1.ChatType.GAME }); } else { socket.emit('error', { message: 'Chat not found' }); } return; } // Check if user has access if (!chat.users.includes(userId)) { socket.emit('error', { message: 'Unauthorized to view this chat' }); return; } socket.emit('chat:history', { chatId: data.chatId, messages: chat.messages, chatInfo: { type: chat.type, name: chat.name, gameId: chat.gameId, users: chat.users } }); } catch (error) { (0, Logger_1.logError)('Error getting chat history', error); socket.emit('error', { message: 'Failed to get chat history' }); } } async handleDeleteChat(socket, data) { try { const userId = socket.userId; const chat = await this.chatRepository.findById(data.chatId); if (!chat) { socket.emit('error', { message: 'Chat not found' }); return; } // Check if user is member of this chat if (!chat.users.includes(userId)) { socket.emit('error', { message: 'Unauthorized to delete this chat' }); return; } // Perform soft delete const deletedChat = await this.chatRepository.softDelete(data.chatId); if (!deletedChat) { socket.emit('error', { message: 'Failed to delete chat' }); return; } // Remove from Redis active chats await this.redisService.removeActiveChat(data.chatId); // Notify all participants that the chat has been deleted this.io.to(data.chatId).emit('chat:deleted', { chatId: data.chatId, deletedBy: userId }); // Remove all users from the chat room for (const participantId of chat.users) { const participantSocket = this.connectedUsers.get(participantId); if (participantSocket) { participantSocket.leave(data.chatId); } // Remove from user's active chats in Redis await this.redisService.removeUserFromChat(participantId, data.chatId); } (0, Logger_1.logAuth)('Chat deleted', userId, { chatId: data.chatId, chatType: chat.type, participantCount: chat.users.length }); socket.emit('chat:delete:success', { chatId: data.chatId, message: 'Chat deleted successfully' }); } catch (error) { (0, Logger_1.logError)('Error deleting chat', error); socket.emit('error', { message: 'Failed to delete chat' }); } } async handleDeleteChatArchive(socket, data) { try { const userId = socket.userId; const archive = await this.chatArchiveRepository.findById(data.archiveId); if (!archive) { socket.emit('error', { message: 'Chat archive not found' }); return; } // Check if user was a participant in the archived chat if (!archive.participants.includes(userId)) { socket.emit('error', { message: 'Unauthorized to delete this chat archive' }); return; } // Hard delete the archive (since it's already archived) await this.chatArchiveRepository.delete(data.archiveId); (0, Logger_1.logAuth)('Chat archive deleted', userId, { archiveId: data.archiveId, originalChatId: archive.chatId, chatType: archive.chatType, participantCount: archive.participants.length }); socket.emit('chat:archive:delete:success', { archiveId: data.archiveId, message: 'Chat archive deleted successfully' }); } catch (error) { (0, Logger_1.logError)('Error deleting chat archive', error); socket.emit('error', { message: 'Failed to delete chat archive' }); } } async handleDeleteMessage(socket, data) { try { const userId = socket.userId; // Check if user has admin/moderator privileges const user = await this.userRepository.findById(userId); if (!user || user.state !== UserAggregate_1.UserState.ADMIN) { // Check if user is admin socket.emit('error', { message: 'Insufficient permissions to delete messages' }); return; } const success = await this.deleteMessage(data.chatId, data.messageId, userId); if (success) { socket.emit('message:delete:success', { chatId: data.chatId, messageId: data.messageId, message: 'Message deleted successfully' }); } else { socket.emit('error', { message: 'Failed to delete message or message not found' }); } } catch (error) { (0, Logger_1.logError)('Error handling delete message request', error); socket.emit('error', { message: 'Failed to delete message' }); } } async handleDisconnection(socket) { const userId = socket.userId; if (userId) { this.connectedUsers.delete(userId); // Update user status in Redis const userData = await this.redisService.getActiveUser(userId); if (userData) { userData.isOnline = false; userData.lastActivity = new Date(); await this.redisService.setActiveUser(userId, userData); } (0, Logger_1.logAuth)('User disconnected from WebSocket', userId, { socketId: socket.id }); } } // Utility methods calculateUnreadMessages(chat, userId) { // Simple implementation - count messages after user's last seen // In production, you'd store lastSeen timestamp per user per chat return chat.messages.filter(msg => msg.userid !== userId).length; } pruneMessages(messages, chatType) { const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000); // Remove messages older than 2 weeks let prunedMessages = messages.filter(msg => new Date(msg.date) > twoWeeksAgo); // For group chats, only apply the 2-week time limit (unlimited messages per user) if (chatType === ChatAggregate_1.ChatType.GROUP) { return prunedMessages.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); } // For direct and game chats, apply both time limit and per-user message limit // Group by user and keep last 10 messages per user const messagesByUser = new Map(); prunedMessages.forEach(msg => { if (!messagesByUser.has(msg.userid)) { messagesByUser.set(msg.userid, []); } messagesByUser.get(msg.userid).push(msg); }); // Keep only last 10 messages per user const finalMessages = []; messagesByUser.forEach((userMessages, userId) => { const last10 = userMessages.slice(-10); finalMessages.push(...last10); }); // Sort by date return finalMessages.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); } async notifyOfflineUsers(chat, message) { // Find users who are not currently connected const offlineUsers = chat.users.filter(userId => userId !== message.userid && !this.connectedUsers.has(userId)); // In a real implementation, you would send push notifications or emails here if (offlineUsers.length > 0) { (0, Logger_1.logRequest)('Offline users to notify', undefined, undefined, { chatId: chat.id, offlineUserCount: offlineUsers.length, messageFrom: message.userid }); } } setupArchivingScheduler() { // Run every hour to check for inactive chats setInterval(async () => { try { // First, cleanup inactive chats from Redis and get their IDs const inactiveChatIds = await this.redisService.cleanupInactiveChats(this.chatTimeout); // Archive the inactive chats in the database for (const chatId of inactiveChatIds) { const chat = await this.chatRepository.findById(chatId); if (chat) { await this.chatRepository.archiveChat(chat); (0, Logger_1.logRequest)('Chat archived due to inactivity', undefined, undefined, { chatId: chat.id, chatType: chat.type, lastActivity: chat.lastActivity, messageCount: chat.messages.length }); } } // Also find inactive chats from database that might not be in Redis const dbInactiveChats = await this.chatRepository.findInactiveChats(this.chatTimeout); const additionalInactiveChats = dbInactiveChats.filter(chat => !inactiveChatIds.includes(chat.id)); for (const chat of additionalInactiveChats) { await this.chatRepository.archiveChat(chat); (0, Logger_1.logRequest)('Chat archived due to inactivity (from DB)', undefined, undefined, { chatId: chat.id, chatType: chat.type, lastActivity: chat.lastActivity, messageCount: chat.messages.length }); } const totalArchived = inactiveChatIds.length + additionalInactiveChats.length; if (totalArchived > 0) { (0, Logger_1.logRequest)('Chat archiving completed', undefined, undefined, { archivedCount: totalArchived, redisCleanedUp: inactiveChatIds.length, databaseCleanedUp: additionalInactiveChats.length, timeoutMinutes: this.chatTimeout }); } // Cleanup old messages from archived chats based on messageCleanupWeeks await this.cleanupOldMessages(); } catch (error) { (0, Logger_1.logError)('Error in chat archiving scheduler', error); } }, 60 * 60 * 1000); // 1 hour // Also run message count cleanup every 5 minutes setInterval(() => { this.cleanupMessageCounts(); }, 5 * 60 * 1000); // 5 minutes } // Public methods for game integration async createGameChat(gameId, gameName, playerIds) { try { const existingGameChat = await this.chatRepository.findByGameId(gameId); if (existingGameChat) { return existingGameChat; } const gameChat = await this.chatRepository.create({ type: ChatAggregate_1.ChatType.GAME, name: gameName, gameId: gameId, users: playerIds, messages: [], lastActivity: new Date() }); // Notify connected players playerIds.forEach(playerId => { const playerSocket = this.connectedUsers.get(playerId); if (playerSocket) { playerSocket.join(gameChat.id); playerSocket.emit('game:chat:created', { chat: { id: gameChat.id, type: gameChat.type, name: gameChat.name, gameId: gameChat.gameId, users: gameChat.users, messages: [] } }); } }); return gameChat; } catch (error) { (0, Logger_1.logError)('Error creating game chat programmatically', error); return null; } } getConnectedUserCount() { return this.connectedUsers.size; } isUserConnected(userId) { return this.connectedUsers.has(userId); } async cleanup() { try { await this.redisService.disconnect(); } catch (error) { (0, Logger_1.logError)('Error during WebSocket service cleanup', error); } } /** * Manually trigger cleanup of old messages and chats * This can be called by admin endpoints for maintenance */ async triggerManualCleanup() { try { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - (this.messageCleanupWeeks * 7)); // Clean up old archived messages const deletedArchivesCount = await this.chatArchiveRepository.cleanup(this.messageCleanupWeeks * 7); // Clean up soft-deleted chats const softDeletedChats = await this.chatRepository.findByPageIncludingDeleted(0, 1000); let deletedChatsCount = 0; for (const chat of softDeletedChats.chats) { if (chat.state === 2 && chat.updateDate < cutoffDate) { // SOFT_DELETE state = 2 await this.chatRepository.delete(chat.id); // Hard delete deletedChatsCount++; } } (0, Logger_1.logRequest)('Manual cleanup triggered', undefined, undefined, { cutoffDate: cutoffDate.toISOString(), cleanupWeeks: this.messageCleanupWeeks, deletedArchives: deletedArchivesCount, deletedChats: deletedChatsCount, triggeredBy: 'manual' }); return { deletedArchives: deletedArchivesCount, deletedChats: deletedChatsCount }; } catch (error) { (0, Logger_1.logError)('Error during manual cleanup', error); throw error; } } /** * Clean up old messages from archived chats based on messageCleanupWeeks setting */ async cleanupOldMessages() { try { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - (this.messageCleanupWeeks * 7)); // Clean up old archived messages using ChatArchiveRepository const deletedArchivesCount = await this.chatArchiveRepository.cleanup(this.messageCleanupWeeks * 7); // Also clean up soft-deleted chats from the main repository // Get all soft-deleted chats that are older than the cleanup period const softDeletedChats = await this.chatRepository.findByPageIncludingDeleted(0, 1000); let deletedChatsCount = 0; for (const chat of softDeletedChats.chats) { if (chat.state === 2 && chat.updateDate < cutoffDate) { // SOFT_DELETE state = 2 await this.chatRepository.delete(chat.id); // Hard delete deletedChatsCount++; } } (0, Logger_1.logRequest)('Old message cleanup completed', undefined, undefined, { cutoffDate: cutoffDate.toISOString(), cleanupWeeks: this.messageCleanupWeeks, deletedArchives: deletedArchivesCount, deletedChats: deletedChatsCount, note: 'Cleanup completed using both ChatRepository and ChatArchiveRepository' }); } catch (error) { (0, Logger_1.logError)('Error cleaning up old messages', error); } } /** * Check if user has exceeded message rate limit * @param userId User ID to check * @returns true if within limit, false if exceeded */ checkMessageRateLimit(userId) { const now = Date.now(); const minute = 60 * 1000; // 1 minute in milliseconds const userStats = this.userMessageCounts.get(userId) || { count: 0, lastReset: now }; // Reset counter if more than a minute has passed if (now - userStats.lastReset >= minute) { userStats.count = 0; userStats.lastReset = now; } // Check if user is within limits if (userStats.count >= this.maxMessagesPerUser) { return false; } // Increment counter userStats.count++; this.userMessageCounts.set(userId, userStats); return true; } /** * Delete a specific message from chat history * This can be used for moderation purposes */ async deleteMessage(chatId, messageId, moderatorUserId) { try { // Get the chat const chat = await this.chatRepository.findById(chatId); if (!chat) { // Check archived chats const archivedChat = await this.chatRepository.getArchivedChat(chatId); if (!archivedChat) { (0, Logger_1.logWarning)('Chat not found for message deletion', { chatId, messageId, moderatorUserId }); return false; } // Remove message from archived chat const updatedMessages = archivedChat.archivedMessages.filter(msg => msg.id !== messageId); if (updatedMessages.length === archivedChat.archivedMessages.length) { (0, Logger_1.logWarning)('Message not found in archived chat', { chatId, messageId, moderatorUserId }); return false; } // Update archived chat await this.chatArchiveRepository.create({ ...archivedChat, archivedMessages: updatedMessages }); (0, Logger_1.logAuth)('Message deleted from archived chat', moderatorUserId, { chatId, messageId, originalMessageCount: archivedChat.archivedMessages.length, newMessageCount: updatedMessages.length }); return true; } // Remove message from active chat const updatedMessages = chat.messages.filter(msg => msg.id !== messageId); if (updatedMessages.length === chat.messages.length) { (0, Logger_1.logWarning)('Message not found in active chat', { chatId, messageId, moderatorUserId }); return false; } // Update active chat await this.chatRepository.update(chatId, { messages: updatedMessages }); // Notify all users in the chat about message deletion this.io.to(chatId).emit('message:deleted', { chatId, messageId, deletedBy: moderatorUserId }); (0, Logger_1.logAuth)('Message deleted from active chat', moderatorUserId, { chatId, messageId, originalMessageCount: chat.messages.length, newMessageCount: updatedMessages.length }); return true; } catch (error) { (0, Logger_1.logError)('Error deleting message', error); return false; } } /** * Clean up old user message count entries (called periodically) */ cleanupMessageCounts() { const now = Date.now(); const minute = 60 * 1000; for (const [userId, stats] of this.userMessageCounts.entries()) { if (now - stats.lastReset >= minute * 5) { // Keep for 5 minutes this.userMessageCounts.delete(userId); } } } } exports.WebSocketService = WebSocketService; //# sourceMappingURL=WebSocketService.js.map