Files
min-chat/server/clients.js
T
Комаров Данил Анатольевич 6 63bf73d2c4 Полный рефакторинг сервера и клиента, голосовые сообщения, аудио/видео, 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
2026-05-29 05:23:22 +03:00

79 lines
1.9 KiB
JavaScript

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 };