Files
SerpentRace/SerpentRace_Backend/dist/Application/Services/WebSocketService.js
T

966 lines
41 KiB
JavaScript

"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