Полный рефакторинг сервера и клиента, голосовые сообщения, аудио/видео, Docker
Сервер: - Вынесена конфигурация в server/config.js (порт, размер файла из env) - Вынесена аутентификация в server/auth.js (загрузка users.json, authenticate()) - Вынесено управление соединениями в server/clients.js (createJsonSender, broadcast, validateFile) - server/server.js стал точкой входа — минимальный код Клиент: - Вынесены хуки: hooks/useWebSocket.js (WebSocket + auth), hooks/useChatMessages.js (сообщения + уведомления) - Вынесены утилиты: utils/blob.js (base64 → Blob URL), utils/linkify.jsx (URL → ссылки), utils/notify.js (звук + favicon) Новые функции: - VoiceRecorder — запись голоса через MediaRecorder, отправка как файл - Аудио/видео плеер в Message (audio/*, video/* с controls) - URL linkification — http/https ссылки автоматически кликабельны - Звуковое уведомление (Web Audio API) при сообщении на неактивной вкладке - Красная точка на favicon при непрочитанных сообщениях Инфраструктура: - docker-entrypoint.sh: авто-перезапуск Node.js сервера при падении - Обновлён README.md: новая структура проекта, список функций, примеры - Кастомный тонкий скроллбар в Message.css
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
const config = require('./config');
|
||||
|
||||
function createJsonSender(ws) {
|
||||
return (data) => {
|
||||
if (ws.readyState === 1) {
|
||||
ws.send(JSON.stringify(data));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createClientHandlers(ws, sendJson) {
|
||||
let authenticated = false;
|
||||
let login = null;
|
||||
|
||||
function getLogin() { return login; }
|
||||
|
||||
function handleAuth(userLogin) {
|
||||
authenticated = true;
|
||||
login = userLogin;
|
||||
console.log(`Authenticated: ${login}`);
|
||||
sendJson({ type: 'auth_result', success: true });
|
||||
return { type: 'system', text: `${login} joined the chat` };
|
||||
}
|
||||
|
||||
function handleAuthFail(reason) {
|
||||
sendJson({ type: 'auth_result', success: false, reason });
|
||||
}
|
||||
|
||||
function wrapHandler(handler) {
|
||||
return (raw) => {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(raw.toString());
|
||||
} catch {
|
||||
sendJson({ type: 'error', message: 'Invalid JSON' });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
if (parsed.type === 'auth') {
|
||||
return handler({ type: 'auth', login: parsed.login, password: parsed.password });
|
||||
}
|
||||
sendJson({ type: 'error', message: 'Authenticate first' });
|
||||
return null;
|
||||
}
|
||||
|
||||
return handler(parsed);
|
||||
};
|
||||
}
|
||||
|
||||
return { getLogin, handleAuth, handleAuthFail, wrapHandler };
|
||||
}
|
||||
|
||||
function formatSizeEstimate(base64Length) {
|
||||
return Math.ceil((base64Length * 3) / 4);
|
||||
}
|
||||
|
||||
function validateFile(parsed) {
|
||||
if (!parsed.filename || !parsed.mime || !parsed.data) {
|
||||
return 'Invalid file message';
|
||||
}
|
||||
const size = formatSizeEstimate(parsed.data.length);
|
||||
if (size > config.maxFileSize) {
|
||||
return `File too large (max ${config.maxFileSize / 1024 / 1024} MB)`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function broadcast(server, message, excludeWs) {
|
||||
const json = JSON.stringify(message);
|
||||
server.clients.forEach(client => {
|
||||
if (client !== excludeWs && client.readyState === 1) {
|
||||
client.send(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { createJsonSender, createClientHandlers, validateFile, broadcast };
|
||||
Reference in New Issue
Block a user