User: const { gpt, gptweb } = require("./API/gpt");
const { dalle } = require("./API/dalle");
const { gpt4o } = require("./API/gpt4o");
const { gpt3 } = require("./API/gpt3.5");
const { prompts } = require("./API/gpt-prompts");
const { blackbox } = require("./API/blackbox");
const { qwen } = require("./API/qwen");
const { gemini } = require("./API/gemini");
const WebSocket = require('ws');
const Bottleneck = require('bottleneck');
const { stats, scheduleDailyReset, resetStatistics } = require('./stats'); // Импортируем модуль статистики
const fs = require('fs');
const path = require('path');
const Ajv = require('ajv');

// Создаем экземпляр Ajv (проверка входящей переписки на соответствие схеме)
const ajv = new Ajv(); 
// Объект для хранения действий и состояния каждого клиента
const clientActions = {};
// Объект для хранения ограничителей по каждому клиенту
const limiters = {}; 
// Замените на ваш домен
const allowedOrigin = 'http://gpt.loc'; 

// Создайте WebSocket-сервер
const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', (ws, req) => {
	
	const urlParams = new URLSearchParams(req.url.split('?')[1]);
    const userId = urlParams.get('token'); // Получаем UUID из URL
    const userIP = req.socket.remoteAddress;
	//console.log("IP-адрес - ", userIP, "Время подключения - ", new Date().toISOString());
	const userHeaders = req.headers;

	// Проверяем Origin
    if (!checkOrigin(userHeaders.origin, ws)) {
        return; // Если отклонено, выходим из обработчика
    }
	
	// Увеличиваем общий счетчик подключений
    stats.totalConnections++;
	
	console.log('Client connected with user ID:', userId, 'Домен - ', userHeaders.origin);
	
	// Инициализируем состояние клиента, если его еще нет
    if (!clientActions[userId]) {
		clientActions[userId] = {
			actions: [], // Массив для хранения действий
			connected: true, // Статус подключения
        };
	}
	else
		clientActions[userId].connected = true;
	
	if (!limiters[userId]) { 
	  // Создаем экземпляр Bottleneck для каждого клиента (ограничение на количество запросов по Id)
		limiters[userId] = new Bottleneck({
            maxConcurrent: 1, // Максимальное количество одновременных задач
            minTime: 1000, // Минимальное время между запросами в миллисекундах
		    highWater: 0 // Не разрешать в очереди больше 0 запросов (игнорировать)
        });
    }

    ws.on('message', (message) => {
		// Проверяем, является ли сообщение экземпляром Buffer
		if (Buffer.isBuffer(message))
			message = message.toString(); 
		try {
			// Преобразуем строку JSON обратно в объект
			message = JSON.parse(message); 
			//Получаем историю чата из запроса
			let messages = message.messages;
			let model = message.model;
			// Выполняем валидацию сообщений
			messages = validateMessages(messages);
			
			// Проверяем, остались ли сообщения после валидации
			if (messages.length === 0) {
				const response = { code: 2, message: "Нет валидных сообщений для обработки." };
				sendMessage(ws, response);
				return;
			}
			
			// Урезаем последнее сообщение до 2000 символов, если оно от клиента
			if (messages[messages.length - 1].role === 'user') {
				messages[messages.length - 1].content = messages[messages.length - 1].content.substring(0, 2000000);
			}
			
			// Увеличиваем общий счетчик запросов
            stats.totalRequests++;
			
			// Используем Bottleneck для управления количеством запросов
			limiters[userId].schedule(() => {
				// Отправка запроса на сервер чата
				if (model == 'gpt-4o')
					return callGPT4o(messages, ws);
				else if (model == 'gemini-pro')
					return callGemini(messages, ws);
				else if (model == 'qwen')
					return callQwen(messages, ws);
				else if (model == 'blackbox')
					return callBlackbox(messages, ws);
				else if (model == 'gpt-3.5-turbo')
					return callGPT35Turbo(messages, ws);
				else
					return callGPT4o(messages, ws);
			}).catch(error => {
				stats.totalIPBlocks++;
				//console.error('Rate limit exceeded, message ignored:', error);
				const response = { code: 2, message: "Превышен лимит запросов. Пожалуйста, попробуйте позже." };
				// Отправка ответа клиенту
				//sendMessage(ws, response);
			});
			
		} catch (error) {
				stats.totalWsMessErrors++;
				console.error('Error parsing JSON:', error);
				const response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
				console.log(response);
				// Отправка ответа клиенту
				sendMessage(ws, response);
		}
		
    });

    ws.on('close', () => {
		clientActions[userId].connected = false; // Обновление статуса подключения
		delete clientActions[userId]; // Удаление состояния клиента
		delete limiters[userId]; // Удаление ограничителя для данного клиента
        console.log('Client ',userId, ' disconnected');
    });
});

// Запускаем планировщик для статистики
scheduleDailyReset();

// Очистка старых соединений раз в минуту (по желанию)
setInterval(() => {
  Object.keys(clientActions).forEach(userId => {
    if (!clientActions[userId].connected) {
      delete clientActions[userId]; // Удаляем пользователя из памяти, если он отключен
	  delete limiters[userId];
    }
  });
}, 10 * 60 * 1000); // Очистка каждые 10 минут


function checkOrigin(origin, socket) {
    if (origin !== allowedOrigin) {
        console.log('Недопустимый домен. Соединение разорвано: ', origin);
        socket.close(); // Закрываем соединение, если домен не разрешен
        return false; // Возвращаем false для индикатора отклонения
    }
    return true; // Возвращаем true, если домен разрешен
}

// Отправляет ответ клиенту
function sendMessage(ws, message) {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(message));
    } else {
        console.warn(`Trying to send message to a closed WebSocket connection.`);
    }
}

// Удаление из массива сообщений не соответствующих схеме
function validateMessages(messages) {
    // Определяем схему для сообщений
    const schema = {
        type: 'object',
        properties: {
            role: { type: 'string' },
            content: { type: 'string' }
        },
        required: ['role', 'content'],
        additionalProperties: false
    };

    // Фильтруем массив, оставляя только корректные сообщения
    return messages.filter(message => {
        // Проверяем, соответствует ли сообщение схеме
        if (ajv.validate(schema, message)) {
            // Обрезаем содержимое, если оно длиннее 2000 символов
            if (message.content.length > 2000000) {
                message.content = message.content.substring(0, 2000000);
            }
            return true; // Сообщение валидно, оставляем в массиве
        }
        return false; // Сообщение не валидно, исключаем из массива
    });
}

console.log('WebSocket сервер запущен через порт 3000');

















//============== ВЫЗОВ API МОДЕЛЕЙ =================================================
//==================================================================================
//==================================================================================
//==================================================================================

// GPT4o
async function callGPT4o(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		//console.log('Запрос клиента к GPT4o===:', messages[messages.length - 1]); 
		console.log('Запрос клиента к GPT4o:', messages); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        gpt4o({
            messages: messages,
            markdown: false
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			try {
				if (!result.finish) {
					// Запоминаем только последний ответ чата (он будет полный, промежуточные ответы - это накопления)
					fullResponse = result.message || "";
					//console.log(result);
				} else {
					// Если завершено, выводим полный ответ
					let response = { code: 1, message: fullResponse};
					console.log('Ответ чата: ', response);
					// Отправка ответа клиенту
					sendMessage(ws, response);
				}
			}
			catch (callbackError) {
				console.error('Error processing callback result:', callbackError);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalGPT4oCallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalGPT4oMessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

// Gemini
async function callGemini(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента к Gemini:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        gemini({
            messages: messages
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			try {
				if (!result.finish) {
					// Запоминаем только последний ответ чата (он будет полный, промежуточные ответы - это накопления)
					fullResponse = result.message || "";
					//console.log(result);
				} else {
					// Если завершено, выводим полный ответ
					let response = { code: 1, message: fullResponse};
					console.log('Ответ чата: ', response);
					// Отправка ответа клиенту
					sendMessage(ws, response);
				}
			}
			catch (callbackError) {
				console.error('Error processing callback result:', callbackError);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalGeminiCallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalGeminiMessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}


// Qwen
async function callQwen(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента к Qwen:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        qwen({
            messages: messages,
            markdown: false
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			try {
				if (!result.finish) {
					// Запоминаем только последний ответ чата (он будет полный, промежуточные ответы - это накопления)
					fullResponse = result.message || "";
					//console.log(result);
				} else {
					// Если завершено, выводим полный ответ
					let response = { code: 1, message: fullResponse};
					console.log('Ответ чата: ', response);
					// Отправка ответа клиенту
					sendMessage(ws, response);
				}
			}
			catch (callbackError) {
				console.error('Error processing callback result:', callbackError);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalQwenCallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalQwenMessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

// Blackbox
async function callBlackbox(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента к Blackbox:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        blackbox({
            messages: messages,
            markdown: false,
            websearch: false,
            llama: false
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			try {
				if (!result.finish) {
					// Запоминаем только последний ответ чата (он будет полный, промежуточные ответы - это накопления)
					fullResponse = result.message || "";
					//console.log(result);
				} else {
					// Если завершено, выводим полный ответ
					let response = { code: 1, message: fullResponse};
					console.log('Ответ чата: ', response);
					// Отправка ответа клиенту
					sendMessage(ws, response);
				}
			}
			catch (callbackError) {
				console.error('Error processing callback result:', callbackError);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalBlackboxCallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalBlackboxMessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}






// GPT3.5-Turbo (Отключен в интерфейсе)
async function callGPT35Turbo(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента к GPT3.5-Turbo:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        gpt3({
            messages: messages,
            markdown: false
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                //throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			try {
				if (!result.finish) {
					// Запоминаем только последний ответ чата (он будет полный, промежуточные ответы - это накопления)
					fullResponse = result.message || "";
					//console.log(result);
				} else {
					// Если завершено, выводим полный ответ
					let response = { code: 1, message: fullResponse};
					console.log('Ответ чата: ', response);
					// Отправка ответа клиенту
					sendMessage(ws, response);
				}
			}
			catch (callbackError) {
				console.error('Error processing callback result:', callbackError);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalGPT35CallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalGPT35MessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

//============== ВЫЗОВ API МОДЕЛЕЙ (КОНЕЦ) ======================

Надо хранить последние время последнего сообщения пользователя. Если последнее сообщение было более часа назад надо закрывать соединение
Assistant:  реализовать функциональность автоматического закрытия соединения WebSocket, если последнее сообщение пользователя было отправлено более часа назад, нужно добавить некоторые изменения в ваш код. 

Вот шаги, которые вам необходимо выполнить:

1. Добавьте свойство для хранения времени последнего сообщения в объект `clientActions`.
2. Обновляйте это время каждый раз, когда пользователь отправляет сообщение.
3. Создайте функцию, которая будет проверять время последнего сообщения и, если оно превышает один час, закрывать соединение.

Вот обновленный пример вашей реализации:

```javascript
const { gpt, gptweb } = require("./API/gpt");
const { dalle } = require("./API/dalle");
const { gpt4o } = require("./API/gpt4o");
const { gpt3 } = require("./API/gpt3.5");
const { prompts } = require("./API/gpt-prompts");
const { blackbox } = require("./API/blackbox");
const { qwen } = require("./API/qwen");
const { gemini } = require("./API/gemini");
const WebSocket = require('ws');
const Bottleneck = require('bottleneck');
const { stats, scheduleDailyReset, resetStatistics } = require('./stats'); 
const fs = require('fs');
const path = require('path');
const Ajv = require('ajv');

// Создаем экземпляр Ajv (проверка входящей переписки на соответствие схеме)
const ajv = new Ajv(); 
const clientActions = {};
const limiters = {}; 
const allowedOrigin = 'http://gpt.loc'; 

const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', (ws, req) => {
    const urlParams = new URLSearchParams(req.url.split('?')[1]);
    const userId = urlParams.get('token'); 
    const userIP = req.socket.remoteAddress;
    const userHeaders = req.headers;

    if (!checkOrigin(userHeaders.origin, ws)) {
        return; 
    }
    
    stats.totalConnections++;
    
    console.log('Client connected with user ID:', userId, 'Домен - ', userHeaders.origin);
    
    if (!clientActions[userId]) {
        clientActions[userId] = {
            actions: [], 
            connected: true,
            lastMessageTime: Date.now()  // Сохраняем время последнего сообщения
        };
    } else {
        clientActions[userId].connected = true;
    }
    
    if (!limiters[userId]) { 
        limiters[userId] = new Bottleneck({
            maxConcurrent: 1,
            minTime: 1000,
            highWater: 0
        });
    }

    ws.on('message', (message) => {
        if (Buffer.isBuffer(message))
            message = message.toString(); 
        
        try {
            message = JSON.parse(message); 
            let messages = message.messages;
            let model = message.model;
            messages = validateMessages(messages);
            
            if (messages.length === 0) {
                const response = { code: 2, message: "Нет валидных сообщений для обработки." };
                sendMessage(ws, response);
                return;
            }
            
            // Устанавливаем текущее время как время последнего сообщения
            clientActions[userId].lastMessageTime = Date.now();

            // Урезаем последнее сообщение до 2000 символов
            if (messages[messages.length - 1].role === 'user') {
                messages[messages.length - 1].content = messages[messages.length - 1].content.substring(0, 2000000);
            }
            
            stats.totalRequests++;
            
            limiters[userId].schedule(() => {
                if (model == 'gpt-4o')
                    return callGPT4o(messages, ws);
                else if (model == 'gemini-pro')
                    return callGemini(messages, ws);
                else if (model == 'qwen')
                    return callQwen(messages, ws);
                else if (model == 'blackbox')
                    return callBlackbox(messages, ws);
                else if (model == 'gpt-3.5-turbo')
                    return callGPT35Turbo(messages, ws);
                else
                    return callGPT4o(messages, ws);
            }).catch(error => {
                stats.totalIPBlocks++;
                const response = { code: 2, message: "Превышен лимит запросов. Пожалуйста, попробуйте позже." };
            });
        } catch (error) {
            stats.totalWsMessErrors++;
            const response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
            sendMessage(ws, response);
        }
    });

    ws.on('close', () => {
        clientActions[userId].connected = false; 
        delete clientActions[userId]; 
        delete limiters[userId]; 
        console.log('Client ',userId, ' disconnected');
    });
});

// Проверка времени последнего сообщения
setInterval(() => {
    Object.keys(clientActions).forEach(userId => {
        const currentTime = Date.now();
        const lastMessageTime = clientActions[userId].lastMessageTime;

        // Если прошло больше одного часа (3600000 миллисекунд)
        if (currentTime - lastMessageTime > 3600000) {
            // Закрываем соединение и очищаем данные
            const ws = [...wss.clients].find(client => client.userId === userId); // Найдем WebSocket клиента
            if (ws) {
                ws.close(); // Закрываем соединение
                console.log('Connection closed for user:', userId, 'due to inactivity.');
            }
            delete clientActions[userId];
            delete limiters[userId];
        }
    });
}, 10000); // Проверка каждый 10 секунд

function checkOrigin(origin, socket) {
    if (origin !== allowedOrigin) {
        socket.close();
        return false; 
    }
    return true; 
}

function sendMessage(ws, message) {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(message));
    } else {
        console.warn(`Trying to send message to a closed WebSocket connection.`);
    }
}

function validateMessages(messages) {
    const schema = {
        type: 'object',
        properties: {
            role: { type: 'string' },
            content: { type: 'string' }
        },
        required: ['role', 'content'],
        additionalProperties: false
    };

    return messages.filter(message => {
        if (ajv.validate(schema, message)) {
            if (message.content.length > 2000000) {
                message.content = message.content.substring(0, 2000000);
            }
            return true; 
        }
        return false; 
    });
}

console.log('WebSocket сервер запущен через порт 3000');

// Остальный вызов API моделей остаются без изменений...
```

В этом коде:
1. Мы добавили свойство `lastMessageTime` в `clientActions[userId]`, которое хранит время последнего сообщения.
2. После обработки каждого сообщения мы обновляем `lastMessageTime`.
3. Мы добавили `setInterval`, который проверяет, прошло ли более часа с момента последнего сообщения, и закрывает соединение, если это так.

Теперь любые пользователи, которые не отправляют сообщения в течение часа, будут автоматически отключены.
User: const ws = [...wss.clients].find(client => client.userId === userId); // Найдем WebSocket клиента
сделай проще
Assistant:  упростить код для закрытия WebSocket соедин в случае неактивности, мы можем обойтись без необходимости хранить `userId` на каждом WebSocket клиенте. Вместо этого мы можем использовать справку от самого WebSocket клиента в функции `on('connection')`. 

Вот обновленный фрагмент кода:

```javascript
// Создаем WebSocket-сервер
const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', (ws, req) => {
    const urlParams = new URLSearchParams(req.url.split('?')[1]);
    const userId = urlParams.get('token'); 
    const userIP = req.socket.remoteAddress;
    const userHeaders = req.headers;

    if (!checkOrigin(userHeaders.origin, ws)) {
        return; 
    }
    
    stats.totalConnections++;
    
    console.log('Client connected with user ID:', userId, 'Домен - ', userHeaders.origin);
    
    clientActions[userId] = {
        actions: [], 
        connected: true,
        lastMessageTime: Date.now(), // Сохраняем время последнего сообщения
        ws // Сохраняем WebSocket клиента
    };

    if (!limiters[userId]) { 
        limiters[userId] = new Bottleneck({
            maxConcurrent: 1,
            minTime: 1000,
            highWater: 0
        });
    }

    ws.on('message', (message) => {
        // Код обработки сообщений...

        // Устанавливаем текущее время как время последнего сообщения
        clientActions[userId].lastMessageTime = Date.now();
    });

    ws.on('close', () => {
        clientActions[userId].connected = false; 
        delete clientActions[userId]; 
        delete limiters[userId]; 
        console.log('Client ', userId, ' disconnected');
    });
});

// Проверка времени последнего сообщения
setInterval(() => {
    const currentTime = Date.now();
    Object.keys(clientActions).forEach(userId => {
        const lastMessageTime = clientActions[userId].lastMessageTime;

        // Если прошло больше одного часа (3600000 миллисекунд)
        if (currentTime - lastMessageTime > 3600000) {
            const ws = clientActions[userId].ws; // Используем сохраненный WebSocket
            if (ws && ws.readyState === WebSocket.OPEN) {
                ws.close(); // Закрываем соединение
                console.log('Connection closed for user:', userId, 'due to inactivity.');
            }
            delete clientActions[userId];
            delete limiters[userId];
        }
    });
}, 10000); // Проверка каждый 10 секунд

// Остальной код...
```

### Изменения:
1. В объекте `clientActions[userId]` теперь хранится ссылка на сам WebSocket (`ws`), что позволяет нам напрямую обращаться к клиенту.
2. Теперь нет необходимости использовать оператор `find`, и мы можем просто получить объект WebSocket из `clientActions`.

Это делает код более простым и эффективным, а также упрощает его понимание.
User: Так будет работать?
setInterval(() => {
  const currentTime = Date.now();
  Object.keys(clientActions).forEach(userId => {
	const lastMessageTime = clientActions[userId].lastMessageTime;
	
    if (!clientActions[userId].connected) {
      delete clientActions[userId]; // Удаляем пользователя из памяти, если он отключен
	  delete limiters[userId];
    }
	else if (currentTime - lastMessageTime > 3600000) {
		const ws = clientActions[userId].ws; // Используем сохраненный WebSocket
		if (ws && ws.readyState === WebSocket.OPEN) {
			ws.close(); // Закрываем соединение
			console.log('Connection closed for user:', userId, 'due to inactivity.');
		}
		delete clientActions[userId];
		delete limiters[userId];
	}
  });
}, 10 * 60 * 1000);
Assistant: